diff --git a/Makefile b/Makefile index 64d7acc0..b09afc37 100644 --- a/Makefile +++ b/Makefile @@ -57,15 +57,15 @@ loader: libdvx libtasks $(MAKE) -C src/loader # Every .wgt recipe runs bin/host/dvxres, so widgets need the tools. -widgets: libdvx libtasks texthelp listhelp tools +widgets: libdvx libtasks texthelp listhelp richtext tools $(MAKE) -C src/widgets/kpunch tools: $(MAKE) -C src/tools -# SETUP.EXE (bin/setup.exe, a static build of the core it needs) and the -# package tool bin/host/dvxpkg. -installer: +# SETUP.EXE (bin/setup.exe, a static build of the core it needs, with +# fonts mkfont makes) and the package tool bin/host/dvxpkg. +installer: tools $(MAKE) -C src/installer # Release media in dist/: the CD image and floppy images (FLOPPY=360K, @@ -218,20 +218,21 @@ include src/sdkHeaders.mk # The SDK's own files (README, sdk.mk, newProject.sh, project templates) # live in src/sdk; its bin/ holds the host tools a project build runs. SDK_SRC = src/sdk -SDK_TOOLS = bin/host/dvxres bin/host/dvxhlpc bin/host/bascomp src/tools/dxeGen.sh +SDK_TOOLS = bin/host/dvxres bin/host/dvxhlpc bin/host/bascomp bin/host/mkfont src/tools/dxeGen.sh -# The host tools come from tools (dvxres, dvxhlpc) and apps (bascomp). +# The host tools come from tools (dvxres, dvxhlpc, mkfont) and apps (bascomp). deploy-sdk: tools apps @echo "Building SDK..." @# Start from an empty tree so a header dropped from the lists goes away. rm -rf $(SDKDIR) - @mkdir -p $(SDKDIR)/bin $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/texthelp $(SDKDIR)/include/listhelp $(SDKDIR)/include/fileops $(SDKDIR)/include/hlpfile $(SDKDIR)/include/xfer $(SDKDIR)/include/conn $(SDKDIR)/include/print $(SDKDIR)/include/game $(SDKDIR)/include/joystick $(SDKDIR)/include/audio $(SDKDIR)/include/music $(SDKDIR)/include/net $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow + @mkdir -p $(SDKDIR)/bin $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/texthelp $(SDKDIR)/include/listhelp $(SDKDIR)/include/richtext $(SDKDIR)/include/fileops $(SDKDIR)/include/hlpfile $(SDKDIR)/include/xfer $(SDKDIR)/include/conn $(SDKDIR)/include/print $(SDKDIR)/include/game $(SDKDIR)/include/joystick $(SDKDIR)/include/audio $(SDKDIR)/include/music $(SDKDIR)/include/net $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow cp $(SDK_CORE_H) $(SDKDIR)/include/core/ cp $(SDK_SHELL_H) $(SDKDIR)/include/shell/ cp $(SDK_TASKS_H) $(SDKDIR)/include/tasks/ cp $(SDK_SQL_H) $(SDKDIR)/include/sql/ cp $(SDK_TEXTHELP_H) $(SDKDIR)/include/texthelp/ cp $(SDK_LISTHELP_H) $(SDKDIR)/include/listhelp/ + cp $(SDK_RICHTEXT_H) $(SDKDIR)/include/richtext/ cp $(SDK_FILEOPS_H) $(SDKDIR)/include/fileops/ cp $(SDK_HLPFILE_H) $(SDKDIR)/include/hlpfile/ cp $(SDK_XFER_H) $(SDKDIR)/include/xfer/ diff --git a/README.md b/README.md index 936e2a20..4ceb00ba 100644 --- a/README.md +++ b/README.md @@ -69,14 +69,15 @@ pinned SHA-256, and unpacks it there (no sudo, nothing outside the project); run it again with `--force` to reinstall: * DJGPP (GCC 12.2.0) and the CWSDPMI stub -- the DOS cross compiler +* UPX 4.2.4 -- packs `SETUP.EXE` to about half its size * 86Box 6.0 and its ROM set, FreeDOS 1.3 boot files and CuteMouse, and DOSBox Staging 0.83.0 -- the emulator test harness * cppcheck 2.21, built from source -- used by the static gate * lrzsz 0.12.20 -- the Terminal tests' ZMODEM peer -It needs curl, tar, bzip2, xz, unzip, make, g++, mtools and xvfb-run. -To build with a different DOS toolchain, pass `DJGPP_PREFIX=...` and -`CWSDSTUB=...` to `make`. +It needs curl, tar, bzip2, xz, unzip, make, g++, mtools, and xvfb-run. +To build with a different DOS toolchain, pass `DJGPP_PREFIX=...`, +`CWSDSTUB=...`, and `UPX=...` to `make`. `make` builds in dependency order: core, tasks, loader, helpers, host tools, widgets, shell, task manager, apps. Build output goes to @@ -92,7 +93,7 @@ subset. The emulators never touch your home directory. See `make dist` builds the release media in `dist/`: a CD image (`DVX.ISO`: `SETUP.EXE` with the DVX package appended, and `README.TXT`) and a set of floppy images (`make dist FLOPPY=720K`; -360K, 720K, 1.2M, 1.44M (default), 2.88M or a number of bytes per +360K, 720K, 1.2M, 1.44M (default), 2.88M, or a number of bytes per disk). On the target machine, type `SETUP` on the CD or Disk 1: Setup installs DVX in a hard-disk folder (and can add it to AUTOEXEC.BAT); the `SETUP.EXE` it leaves in that folder uninstalls. `./mkcd.sh` wraps @@ -130,7 +131,7 @@ dvxgui/ See `src/tools/README.md` for the host-side development tools (resource tool, icon generators, help compiler, proxy), and `src/apps/kpunch/README.md` for the SDK contract applications must -follow. `make` also writes the SDK to `bin/sdk`: headers, host tools +follow. `make` also writes the SDK to `bin/sdk`: headers, host tools, and a template for each kind of module (`bin/sdk/newProject.sh app NAME`); the SDK Quick Start in the system help walks through it. diff --git a/cppcheck.sh b/cppcheck.sh index 8d83e3c7..b46d613f 100755 --- a/cppcheck.sh +++ b/cppcheck.sh @@ -24,6 +24,7 @@ INCLUDES=( -Isrc/libs/kpunch/libtasks -Isrc/libs/kpunch/texthelp -Isrc/libs/kpunch/listhelp + -Isrc/libs/kpunch/richtext -Isrc/libs/kpunch/dvxshell -Isrc/libs/kpunch/taskmgr -Isrc/libs/kpunch/serial diff --git a/docs/dvx_basic_reference.html b/docs/dvx_basic_reference.html index 13736413..49a3d62c 100644 --- a/docs/dvx_basic_reference.html +++ b/docs/dvx_basic_reference.html @@ -79,7 +79,7 @@ img { max-width: 100%; }
  • Flow Control
  • Input/Output
  • Miscellaneous Statements
  • -
  • Sound, Joysticks and Frames
  • +
  • Sound, Joysticks, and Frames
  • Console and Graphics
  • @@ -144,6 +144,8 @@ img { max-width: 100%; }
  • ListView
  • ProgressBar
  • OptionButton
  • +
  • RichEdit
  • +
  • RichLabel
  • ScrollPane
  • Decorative Controls
  • Line
  • @@ -182,10 +184,13 @@ img { max-width: 100%; }
  • AddChildItem
  • AddItem
  • AddItem
  • +
  • AddLineBreak
  • AddMenuItem
  • AddMenuSeparator
  • AddNew
  • +
  • AddParagraph
  • AddSubMenu
  • +
  • AddText
  • Alignment
  • Alignment
  • AND
  • @@ -415,10 +420,15 @@ img { max-width: 100%; }
  • DestroyMenu
  • Development Environment
  • DIM
  • +
  • DLG_FONT_FIXED
  • DlgButtons
  • DlgColor
  • DlgFileOpenIn$
  • DlgFileSaveIn$
  • +
  • DlgFont
  • +
  • DlgFontName$
  • +
  • DlgFontSize
  • +
  • DlgFontStyle
  • DlgInputCancelled
  • DlgIntInput
  • DlgIntInputStep
  • @@ -533,7 +543,25 @@ img { max-width: 100%; }
  • FmRawAcquire
  • FmWrite
  • Focus rectangle
  • +
  • Font (form)
  • +
  • Font Dialog
  • +
  • Font properties
  • +
  • Font row
  • +
  • font.bas
  • +
  • FontBold
  • +
  • FontFamilyCount
  • +
  • FontFamilyName$
  • +
  • FontItalic
  • +
  • FontName
  • +
  • FontName (form)
  • +
  • Fonts
  • +
  • Fonts (controls)
  • +
  • Fonts Section
  • FontScale
  • +
  • FontSize
  • +
  • FontStrikethru
  • +
  • FontTextWidth
  • +
  • FontUnderline
  • FOR
  • FOR APPEND
  • FOR BINARY
  • @@ -691,6 +719,7 @@ img { max-width: 100%; }
  • ListView
  • LOAD
  • Load
  • +
  • LoadFile
  • LoadFormText
  • LOADFORMTEXT
  • LOADPICTURE
  • @@ -881,6 +910,7 @@ img { max-width: 100%; }
  • Project Explorer
  • Project Files
  • Project files
  • +
  • Project Fonts
  • Project Properties
  • Project System
  • Project Type
  • @@ -937,10 +967,16 @@ img { max-width: 100%; }
  • RESUME NEXT
  • RETURN
  • RGB
  • +
  • RichEdit
  • +
  • RichEdit
  • +
  • RichLabel
  • +
  • RichLabel
  • +
  • richtext.bas
  • RIGHT$
  • RMDIR
  • RND
  • RSET
  • +
  • RTF
  • RTRIM$
  • Run
  • Run Menu
  • @@ -956,6 +992,7 @@ img { max-width: 100%; }
  • Save File
  • Save on Run
  • Save Project
  • +
  • SaveFile
  • SaveScreenshot
  • Scale
  • ScaleHeight
  • @@ -989,14 +1026,23 @@ img { max-width: 100%; }
  • SecSha256$
  • security.bas
  • SEEK
  • +
  • SelAlignment
  • +
  • SelBold
  • +
  • SelColor
  • Select All
  • SELECT CASE
  • SelectAll
  • SelectedRows
  • +
  • SelFontName
  • +
  • SelFontSize
  • +
  • SelItalic
  • SelLength
  • +
  • SelRTF
  • SelStart
  • +
  • SelStrikethru
  • SelText
  • SelText
  • +
  • SelUnderline
  • SendKeys
  • Separator
  • Separator
  • @@ -1005,9 +1051,11 @@ img { max-width: 100%; }
  • SET
  • SetActive
  • SETATTR
  • +
  • SetColor
  • SetColumns
  • SetCursor
  • SetEvent
  • +
  • SetFont
  • SetItemTag
  • SetLineMarker
  • SetPenColor
  • @@ -1017,6 +1065,7 @@ img { max-width: 100%; }
  • SetSelected
  • SetSort
  • SetStep
  • +
  • SetStyle
  • SetSyntaxColor
  • Settings
  • Settings Files
  • @@ -1149,6 +1198,7 @@ img { max-width: 100%; }
  • SysSetColor
  • SysSetMouse
  • SysSetMousePos
  • +
  • SysSetTextSmoothing
  • SysSetVideoMode
  • SysSetWallpaper
  • SysShiftState
  • @@ -1167,9 +1217,11 @@ img { max-width: 100%; }
  • Telnet
  • Terminal
  • Text
  • +
  • Text smoothing
  • TextArea
  • TextBox
  • TextHeight
  • +
  • TextRTF
  • TextWidth
  • TextWidthAccel
  • THEN
  • @@ -1434,13 +1486,13 @@ img { max-width: 100%; } --- Project Properties... Edit project metadata (name, author, version, startup form, icon, etc.) and the command line arguments used when the program is run from the IDE. --- - Add File... Ctrl+O Add a .bas, .frm, .cls or .ctl file to the project. If no project is open, creates an implicit project from the file. A file that cannot be read, or is 64 KB or larger, is refused with a message. + Add File... Ctrl+O Add a .bas, .frm, .cls, or .ctl file to the project. If no project is open, creates an implicit project from the file. A file that cannot be read, or is 64 KB or larger, is refused with a message. New Class Module Add a new, empty class module (Class1.cls, Class2.cls, ...) to the project and open it in the Code Editor. See Class Modules under Project System. New UserControl Add a new UserControl, a control of your own (UserControl1.ctl, UserControl2.ctl, ...), to the project and open it in the Code Editor. See UserControls in the IDE. Save File Ctrl+S Save the active file to disk. Save All Save all modified files in the project. --- - Make Executable... Compile the project and save it as a standalone .app file. Prompts for a debug or release build. A release build leaves out debugging information and gives forms and controls short internal names, except in a program that uses names as text (the Name property, Controls, DesignSelection or a DesignMouse event procedure), which keeps them. + Make Executable... Compile the project and save it as a standalone .app file. Prompts for a debug or release build. A release build leaves out debugging information and gives forms and controls short internal names, except in a program that uses names as text (the Name property, Controls, DesignSelection, or a DesignMouse event procedure), which keeps them. Make Control... In a control project (Project Properties, Type Control) this item replaces Make Executable: it compiles the project and saves its controls as a control file (.wgt, named after the project). See UserControls in the IDE. --- Rename File... Give the file selected in the Project Explorer a new name, on disk and in the project. See Renaming a File under Project System. @@ -1467,7 +1519,7 @@ img { max-width: 100%; } Find... Ctrl+F Open the Find/Replace dialog. Find Next F3 Find the next occurrence of the search text (also after the dialog is closed). Replace... Ctrl+H Open the Find/Replace dialog with replace enabled. -

    Cut, Copy, Paste, Select All and Delete act on the window you last worked in: the form designer's selected control, or the text of the code, output or immediate window.

    +

    Cut, Copy, Paste, Select All, and Delete act on the window you last worked in: the form designer's selected control, or the text of the code, output, or immediate window.

    Run Menu

    Back to Overview

    @@ -1478,7 +1530,7 @@ img { max-width: 100%; } Run F5 Compile and run the program. If paused at a breakpoint, resumes execution with debugging disabled (runs free). Debug Shift+F5 Compile and run with the debugger active. Breakpoints are active but execution does not pause at the first statement. If paused, resumes to the next breakpoint. Run Without Recompile Ctrl+F5 Re-run the last compiled module without recompiling. - Stop Esc Stop the running program. Run, Debug and the project commands stay disabled until the program has finished shutting down. + Stop Esc Stop the running program. Run, Debug, and the project commands stay disabled until the program has finished shutting down. --- Step Into F8 Execute one statement, stepping into SUB/FUNCTION calls. If idle, starts a debug session and breaks at the first statement. Step Over Shift+F8 Execute one statement, stepping over SUB/FUNCTION calls. @@ -1584,7 +1636,7 @@ img { max-width: 100%; }
  • Object -- lists (General) plus all objects (form name, control names, menu item names, and the WithEvents variables the file declares). Selecting an object filters the Function dropdown.
  • Function -- lists all event handlers (procedures) for the selected object. Implemented handlers are listed first (plain text); unimplemented handlers follow in brackets (e.g., [Click]). Selecting an unimplemented event creates a new event handler stub. Only the events the object can get are listed: Paint, for example, appears for the form and for PictureBoxes, the objects a program draws on.
  • -

    A WithEvents variable (Dim WithEvents acct As Account, also Private or Public, in a module, a form or a class module) is listed as an object; its events are the ones its class module declares with Event. Picking one of them creates the handler acct_EventName with the event's own parameters, so it matches what the class raises. Selecting the variable when it has no handler yet creates the handler for the class's first event, as a control's default event is created.

    +

    A WithEvents variable (Dim WithEvents acct As Account, also Private or Public, in a module, a form, or a class module) is listed as an object; its events are the ones its class module declares with Event. Picking one of them creates the handler acct_EventName with the event's own parameters, so it matches what the class raises. Selecting the variable when it has no handler yet creates the handler for the class's first event, as a control's default event is created.

    The editor shows one procedure at a time. Each procedure has its own buffer, and switching between them is instantaneous. The (General) section contains module-level declarations and code.

    Text outside the procedure being shown is never lost: lines typed above or below the SUB move to (General), a second SUB typed into the buffer becomes a procedure of its own, and a procedure whose SUB line was deleted becomes (General) text. When you leave an event handler that was generated and never edited it is removed; empty SUBs and FUNCTIONs you wrote yourself are kept. After a save the editor shows exactly what was stored (blank lines around a procedure are normalized). A section too large for the editor opens read-only, with a message in the status bar.

    Syntax Highlighting

    @@ -1598,7 +1650,7 @@ img { max-width: 100%; } Comments ' This is a comment, REM This is a comment Numbers 42, 3.14, &HFF, date literals such as #2026-09-30# and #1/15/2026 10:30 AM# Operators =, <, >, +, -, *, /, \, & -

    Date literals are colored as numbers only when they are real dates, as the compiler reads them; the # of a file number (CLOSE #1) is not. Some words are colored like keywords without being reserved -- built-in type names, Empty, Null, EACH, EVENT, WITHEVENTS, and the console and graphics statements such as CLS, LOCATE, COLOR, SCREEN, PSET, CIRCLE, PAINT, PCOPY and PMAP. A program may use them as its own names, and after a dot (Text1.Width, Picture1.Point) they are shown as ordinary names.

    +

    Date literals are colored as numbers only when they are real dates, as the compiler reads them; the # of a file number (CLOSE #1) is not. Some words are colored like keywords without being reserved -- built-in type names, Empty, Null, EACH, EVENT, WITHEVENTS, and the console and graphics statements such as CLS, LOCATE, COLOR, SCREEN, PSET, CIRCLE, PAINT, PCOPY, and PMAP. A program may use them as its own names, and after a dot (Text1.Width, Picture1.Point) they are shown as ordinary names.

    Editor Features

    -

    Opening a form in the designer does not mark it modified. A control whose Visible property is False is still shown on the design surface (the setting applies when the program runs). Disabling a container greys the controls inside it; a control's own Enabled = False is kept when the container is enabled again. A .frm file without an AutoSize line opens without AutoSize; AutoSize = True wins over Width and Height lines. Text values (Caption, Text and so on) are limited to 379 characters.

    +

    Opening a form in the designer does not mark it modified. A control whose Visible property is False is still shown on the design surface (the setting applies when the program runs). Disabling a container greys the controls inside it; a control's own Enabled = False is kept when the container is enabled again. A .frm file without an AutoSize line opens without AutoSize; AutoSize = True wins over Width and Height lines. Text values (Caption, Text, and so on) are limited to 379 characters.

    Menu Editor

    -

    The Menu Editor (View > Menu Editor, Ctrl+E) edits the active form's menu bar and popup menus. Menus nest at most 15 levels (0-14); indenting refuses when any item below would go deeper. OK rejects an item nested more than one level below the item above it, and a blank item that has sub-items (it must be named); blank items without sub-items are dropped. The window's close box cancels. Shortcut is a key that runs the item (F2, Ctrl+X, Alt+Enter ...), shown at its right; Text is key text shown without making a shortcut (see Menu Item Properties); OK rejects a Shortcut it does not know. Renaming a menu item renames its event handlers in the code and updates the ContextMenu setting of the form and its controls; inserting, deleting and moving items does not rename anything.

    +

    The Menu Editor (View > Menu Editor, Ctrl+E) edits the active form's menu bar and popup menus. Menus nest at most 15 levels (0-14); indenting refuses when any item below would go deeper. OK rejects an item nested more than one level below the item above it, and a blank item that has sub-items (it must be named); blank items without sub-items are dropped. The window's close box cancels. Shortcut is a key that runs the item (F2, Ctrl+X, Alt+Enter ...), shown at its right; Text is key text shown without making a shortcut (see Menu Item Properties); OK rejects a Shortcut it does not know. Renaming a menu item renames its event handlers in the code and updates the ContextMenu setting of the form and its controls; inserting, deleting, and moving items does not rename anything.

    Form Properties

    -

    Forms have the following design-time properties: Name, Caption, Width, Height, Left, Top, Layout (VBox or HBox), Centered, AutoSize, Resizable, HelpTopic and ContextMenu.

    +

    Forms have the following design-time properties: Name, Caption, Width, Height, Left, Top, Layout (VBox or HBox), Centered, AutoSize, Resizable, HelpTopic, ContextMenu, and the font (the Font row; see Properties Panel).

    Properties Panel

    Toolbox

    Back to Overview

    @@ -1643,7 +1695,8 @@ img { max-width: 100%; }
  • More Than One Copy -- "Allow more than one copy to run at once" ([Settings] MultiInstance = True): the compiled program can be started again while it is running, and each copy runs on its own (like the Clock and the File Manager). Without it, starting the program again only says that it is already running.
  • Help File -- a compiled help file (.hlp) for the program. It is copied next to the compiled program, and F1 in the running program opens it, at the HelpTopic of the focused control or, failing that, of the form. Add help.bas to the project and call HelpView to open it from a Help menu.
  • Type -- what the project makes: a program (an .app, the default) or, with Type = Control, a control file (a .wgt holding the project's UserControls for every program). See UserControls in the IDE.
  • -
  • File list -- relative paths of all the project's files: modules (.bas) in the [Modules] section, class modules (.cls) in the [Classes] section, UserControls (.ctl) in the [Controls] section and forms (.frm) in the [Forms] section.
  • +
  • File list -- relative paths of all the project's files: modules (.bas) in the [Modules] section, class modules (.cls) in the [Classes] section, UserControls (.ctl) in the [Controls] section, and forms (.frm) in the [Forms] section.
  • +
  • Fonts -- the .DFN fonts the program carries, in the [Fonts] section (see Fonts a Program Carries).
  • The file sections all have the same shape: one FileN line per file, numbered from 0, and a Count line:

    [Modules]
    @@ -1658,15 +1711,18 @@ Count = 2
     [Forms]
     File0 = form1.frm
     Count = 1
    +

    Fonts a Program Carries

    +

    A program can carry fonts of its own, so its text looks the same whatever fonts were installed. In Project > Properties, Add... in the Fonts row picks a .DFN font (one outside the project's directory can be copied into it); Remove takes the chosen one off the list (the file stays). The fonts are listed in the [Fonts] section of the project file. Make builds them into the program as font resources, and the program has them whenever it runs; running it from the IDE gives it the same fonts until it stops. A program carries at most 64 fonts.

    +

    A program's fonts are its own: other programs never see them. Where a font it carries is of a family that is installed too, its sizes and styles come first and the installed family fills in the rest. Fonts are made with the SDK's mkfont from TrueType, Type 1, and bitmap fonts -- one size and only the characters the program uses keeps a font small -- or copied from DVX's FONTS folder. Each .DFN carries the notice and licence of the font it was made from.

    How Projects are Compiled

    -

    When the project is compiled, all source files are concatenated into a single source stream: the modules first, then the class modules, then the UserControls, then the forms. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas, .cls or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form, and the code of each class module and UserControl is likewise marked with its class name; the header lines of a class module or UserControl are not compiled.

    +

    When the project is compiled, all source files are concatenated into a single source stream: the modules first, then the class modules, then the UserControls, then the forms. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas, .cls, or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form, and the code of each class module and UserControl is likewise marked with its class name; the header lines of a class module or UserControl are not compiled.

    Compile-Time Validation

    Before the source is handed to the compiler, the IDE scans every .frm file and cross-checks the forms against the widget registry and the statements that reference them. Three classes of error are caught and reported without producing a binary:

    These checks use the same metadata the Properties panel and Toolbox read from each installed control, so property and method names always match the current widget set without any separate list to maintain.

    Project Operations

    @@ -1676,19 +1732,19 @@ Count = 1 Open Project Opens a .dbp file and loads all referenced files into memory. A project file that is 64 KB or larger, or cannot be read, is not loaded; opening it later reports "Cannot load <file>: unreadable or larger than 65535 bytes." Carriage returns are removed when files are loaded. Save Project Writes the .dbp file to disk. Close Project Closes the project, prompting to save unsaved changes. - Add File Adds a .bas, .frm, .cls or .ctl file to the project. Opening a file without a project auto-creates an implicit project. + Add File Adds a .bas, .frm, .cls, or .ctl file to the project. Opening a file without a project auto-creates an implicit project. New Class Module Adds a new class module to the project and opens it (see Class Modules below). New UserControl Adds a new UserControl to the project and opens it (see UserControls in the IDE). Rename File Gives a file a new name on disk and in the project file (see Renaming a File below). Remove File Removes a file from the project (prompts to save if modified). Removing the startup form clears the Startup Form setting. - Project Properties Opens a dialog for editing project metadata (name, type, author, company, version, copyright, description, icon, help file, startup form), whether more than one copy may run at once, and the Arguments for runs from the IDE. + Project Properties Opens a dialog for editing project metadata (name, type, author, company, version, copyright, description, icon, help file, startup form), the fonts the program carries, whether more than one copy may run at once, and the Arguments for runs from the IDE.

    Renaming a File

    -

    Select a file in the Project Explorer and choose File > Rename File. Type the new file name; the extension may be left out (a module stays .bas, a class module .cls, a UserControl .ctl and a form .frm, and cannot be given another kind's extension). The file is renamed in the project's directory and the project file is saved at once with the new name, so the two always agree. Nothing changes, and a message says why, when the name is not a valid file name, when another project file or any file in the directory already has it, or when the file or the project file cannot be written -- an existing file is never overwritten. A file that has never been saved is simply given the new name, and is written under it when you save.

    -

    A class module named after its file (it has no Attribute VB_Name line, or one that matches the file name, as File > New Class Module writes it) takes the new name as its class name: its Attribute VB_Name line is changed to match (an edit that is saved with the file, like any other). The new name must then be a valid name that no other form, class module or UserControl uses. Code that uses the old class name is not changed. A UserControl named after its file (as File > New UserControl makes it) takes the new name too, on its Begin UserControl line; forms that use the old name are not changed. A form keeps its Name property; rename the form itself in the Properties window.

    +

    Select a file in the Project Explorer and choose File > Rename File. Type the new file name; the extension may be left out (a module stays .bas, a class module .cls, a UserControl .ctl, and a form .frm, and cannot be given another kind's extension). The file is renamed in the project's directory and the project file is saved at once with the new name, so the two always agree. Nothing changes, and a message says why, when the name is not a valid file name, when another project file or any file in the directory already has it, or when the file or the project file cannot be written -- an existing file is never overwritten. A file that has never been saved is simply given the new name, and is written under it when you save.

    +

    A class module named after its file (it has no Attribute VB_Name line, or one that matches the file name, as File > New Class Module writes it) takes the new name as its class name: its Attribute VB_Name line is changed to match (an edit that is saved with the file, like any other). The new name must then be a valid name that no other form, class module, or UserControl uses. Code that uses the old class name is not changed. A UserControl named after its file (as File > New UserControl makes it) takes the new name too, on its Begin UserControl line; forms that use the old name are not changed. A form keeps its Name property; rename the form itself in the Properties window.

    Project Explorer

    -

    The Project Explorer is a tree view window listing all project files, in four groups: Forms, Modules, Classes and Controls (UserControls). Double-click a file to open it: .bas, .cls and .ctl files open in Code view, .frm files open in Design view. The selected file in the Project Explorer determines the target for View > Code and View > Designer commands.

    +

    The Project Explorer is a tree view window listing all project files, in four groups: Forms, Modules, Classes, and Controls (UserControls). Double-click a file to open it: .bas, .cls, and .ctl files open in Code view, .frm files open in Design view. The selected file in the Project Explorer determines the target for View > Code and View > Designer commands.

    Class Modules

    -

    A class module (.cls file) defines a kind of object for your program: its member variables, properties and methods. The BASIC Language Reference describes how to write one (see Objects and Class Modules); this section covers working with them in the IDE.

    +

    A class module (.cls file) defines a kind of object for your program: its member variables, properties, and methods. The BASIC Language Reference describes how to write one (see Objects and Class Modules); this section covers working with them in the IDE.

    Each control type exposes different properties (e.g., Caption, Text, Width, Height, MaxWidth, MaxHeight, Weight, Alignment, Enabled, Visible, and type-specific properties like DataSource and DataField for data-bound controls). A UserControl placed on a form lists the properties its code defines.

    -

    While a UserControl (.ctl) is the active file, the panel shows the control's header instead: Name, Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description and DefaultEvent (see UserControls in the IDE).

    +

    The Font row shows the font the selected control, or the form, draws with: its family, size in pixels, and styles, such as "DejaVu Sans, 14, Bold". Double-click it to choose a font in the Font dialog; the design surface shows it at once. The choice is kept as the six Font properties (FontName, FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru), which then appear in the list and can be changed one at a time too. Choosing the font the control would have anyway (its container's or the form's) removes them, so the control follows the form's font again. A font chosen for the form is used by every control that has none of its own. For a TextBox, TextArea, ComboBox, SpinButton, or Terminal the dialog lists only fixed-pitch families.

    +

    While a UserControl (.ctl) is the active file, the panel shows the control's header instead: Name, Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, and DefaultEvent (see UserControls in the IDE).

    Form Designer

    UserControls in the IDE

    Back to Overview

    @@ -1774,6 +1831,7 @@ End Sub

    Toolbox

    The Toolbox (Window > Toolbox) is a floating palette of buttons, one for each available control type. Every widget registered with DVX that has a BASIC name appears in the toolbox: the controls DVX comes with, the controls written in BASIC that are installed for every program (control files in the WIDGETS folder), and the open project's own UserControls (see UserControls in the IDE). The buttons are in alphabetical order of the control's name. A button shows the control's picture, or its name when it has none; its tooltip is the control's description.

    +

    The Toolbox opens four buttons across and as tall as its buttons need, down to the bottom of the screen. Resize it like any window: the buttons rearrange to fill its width, and when they do not all fit, a scroll bar reaches the rest.

    Click a tool to select it (the active tool name is stored in the designer state), then click on the form to place a new instance. Click the same tool again to deselect it and return to pointer mode.

    Available Controls

      VB Name         Description
    @@ -1795,6 +1853,8 @@ End Sub
    OptionButton Radio button (mutually exclusive within a group). PictureBox Canvas for drawing and images. ProgressBar Progress indicator bar. + RichEdit Text box for text in many fonts, styles, and colors. + RichLabel Text in many fonts, styles, and colors, in aligned paragraphs. ScrollPane Scrollable container for large content. Spacer Invisible spacing element for layout. SpinButton Numeric up/down spinner. @@ -1851,7 +1911,7 @@ End Sub
  • The current debug line (when paused) has a yellow background.
  • Breakpoint Adjustment on Edit

    -

    Breakpoints follow their statements when lines are added, removed, joined, split or pasted. A breakpoint whose line is deleted is removed.

    +

    Breakpoints follow their statements when lines are added, removed, joined, split, or pasted. A breakpoint whose line is deleted is removed.

    Stepping

      Action          Shortcut         Behavior
       ------          --------         --------
    @@ -1982,7 +2042,7 @@ End Sub

    Output Window

    The Output window is a read-only TextArea at the bottom-left of the screen. It displays:

    @@ -2006,7 +2066,7 @@ End Sub

    Buttons

      Button        Action
       ------        ------
    -  Find Next     Find the next occurrence. Function scope searches the current procedure, Object scope the selected object's procedures, File scope the current file and Project scope every file. At the end of the current procedure the search wraps around to its start.
    +  Find Next     Find the next occurrence. Function scope searches the current procedure, Object scope the selected object's procedures, File scope the current file, and Project scope every file. At the end of the current procedure the search wraps around to its start.
       Replace       Replace the current match and find the next one.
       Replace All   Replace all occurrences within the selected scope.
       Close         Close the dialog.
    @@ -2035,7 +2095,7 @@ End Sub Copyright Default copyright notice. (empty) Description Default project description (one line). (empty)

    Colors Tab

    -

    Customizes the syntax-highlighting colors used in the code editor. The left side is a list of seven color entries (Default Text, Keywords, Strings, Comments, Numbers, Operators, Types). Select an entry to see its color, and its red, green and blue amounts, on the right. Click Change... (or double-click the entry) to pick a new color in the color box: click a basic color or mix one with the Red, Green and Blue sliders or number boxes, then click OK. Changes are applied to the open editor when you click OK in the Preferences dialog.

    +

    Customizes the syntax-highlighting colors used in the code editor. The left side is a list of seven color entries (Default Text, Keywords, Strings, Comments, Numbers, Operators, Types). Select an entry to see its color, and its red, green, and blue amounts, on the right. Click Change... (or double-click the entry) to pick a new color in the color box: click a basic color or mix one with the Red, Green, and Blue sliders or number boxes, then click OK. Changes are applied to the open editor when you click OK in the Preferences dialog.

    Back to Overview

    @@ -2070,20 +2130,20 @@ End Sub

    Changes in This Release

    -

    This release fixes a large number of problems in the language, the runtime, the controls and the IDE. Most programs keep working unchanged. Three changes need attention in existing programs: variable scope (see Module Variables Need SHARED), the order of the bytes in a color, and text stored in numbers (see Colors and Numbers Follow Visual Basic).

    +

    This release fixes a large number of problems in the language, the runtime, the controls, and the IDE. Most programs keep working unchanged. Three changes need attention in existing programs: variable scope (see Module Variables Need SHARED), the order of the bytes in a color, and text stored in numbers (see Colors and Numbers Follow Visual Basic).

    Module Variables Need SHARED

    DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable declared at module level (including the code section of a form) only when it is declared with DIM SHARED, or named in a SHARED statement inside the procedure. Before, every module variable was visible everywhere.

    See: Variable Scope and SHARED

    Colors and Numbers Follow Visual Basic

    See: Color Functions

    @@ -2091,59 +2151,59 @@ End Sub

    Language

    -

    Objects, Classes and Variants

    +

    Objects, Classes, and Variants

    See: Objects and Class Modules

    See: Events

    See: Interfaces (Implements)

    See: Variants

    -

    Sound, Joysticks and Frames

    +

    Sound, Joysticks, and Frames

    -

    See: Sound, Joysticks and Frames

    +

    See: Sound, Joysticks, and Frames

    Runtime

    @@ -2199,11 +2259,12 @@ End Sub
  • A reference to a removed control or to a control of an unloaded form raises an "unknown control" runtime error. CreateControl with a parent that is not a container on the same form raises "CreateControl: invalid parent".
  • SetEvent works on forms (Me) and menu items; a handler without parameters is called without the event's arguments.
  • Frame: the Caption from the .frm file or set by code now shows on the frame's border, and reading Caption gives it back (it was ignored and read back "").
  • -
  • A control's Index below 0, in a .frm file or typed in the designer, means the control is not in a control array, in the designer, the form loader and the saved file alike (a negative Index was kept, saved without an Index line and read back as a different control).
  • +
  • A control's Index below 0, in a .frm file or typed in the designer, means the control is not in a control array, in the designer, the form loader, and the saved file alike (a negative Index was kept, saved without an Index line, and read back as a different control).
  • ListBox: a horizontal scroll bar appears when the box is narrower than its longest item (its MaxWidth, or a narrow form); Left and Right scroll it a character at a time.
  • -
  • Padding = 0 and Spacing = 0 now mean no padding and no space (before, 0 meant the default). A form, box or ScrollPane that sets none keeps its own (4 pixels for forms, VBox and HBox; a Frame's Spacing is 0, a ScrollPane's both 0), and reading Padding or Spacing gives the pixels in use. In the designer, an empty Padding or Spacing on the form means the layout's own.
  • +
  • Padding = 0 and Spacing = 0 now mean no padding and no space (before, 0 meant the default). A form, box, or ScrollPane that sets none keeps its own (4 pixels for forms, VBox, and HBox; a Frame's Spacing is 0, a ScrollPane's both 0), and reading Padding or Spacing gives the pixels in use. In the designer, an empty Padding or Spacing on the form means the layout's own.
  • MaxWidth and MaxHeight win over MinWidth / MinHeight and over the size a control's contents ask for, and the space above the cap goes to the other controls instead of staying empty.
  • -
  • New drawing members on forms and PictureBoxes: FontScale (Print, TextWidth and TextHeight up to 8 times as large, for titles) and ScrollBarsNeeded (which scroll bars content of a size needs in an area, as the controls decide it; vbSBNone, vbHorizontal, vbVertical, vbBoth).
  • +
  • New drawing members on forms and PictureBoxes: FontScale (Print, TextWidth, and TextHeight up to 8 times as large, for titles) and ScrollBarsNeeded (which scroll bars content of a size needs in an area, as the controls decide it; vbSBNone, vbHorizontal, vbVertical, vbBoth).
  • +
  • Fonts: every control and form has FontName, FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru, as in Visual Basic. A control without a font of its own uses its container's or its form's. Print, TextWidth, TextHeight, PrintAccel, and TextWidthAccel on forms and PictureBoxes use the object's font. The new DlgFont in commdlg.bas shows the font dialog.
  • Form.Visible = True/False behaves exactly like Show/Hide, but only on a real change: Visible = True on a form that is already shown does nothing (a modal form stays modal), and on a form that has no window it is ignored. MinWidth and MinHeight read back the value that was set. Size properties are limited to 0..32767.
  • Disabling a container (for example Frame.Enabled = False) blocks mouse and keyboard input to every control inside it (also inside a disabled ScrollPane or Splitter), and SetFocus on such a control does nothing.
  • OptionButton.Value is a read/write Boolean (True = selected); the button's index is the new read-only OptionIndex. Setting Value raises no Click.
  • @@ -2211,14 +2272,14 @@ End Sub
  • ListBox SetItems "" empties the list, and every | separates items. ListView SetColumns has no length limit. TreeView Clear resets scrolling. TextArea FindNext finds a match that starts at the cursor, and the new SetCursor method moves the caret without selecting.
  • TabStrip/TabControl: a negative tab index selects the first tab and an index past the end the last one.
  • The mouse wheel over a DBGrid scrolls the rows without moving the current record, and over a Terminal scrolls its history locally.
  • -
  • .frm files: End followed by blanks or a comment (also End'comment) closes its block while End followed by a word does not, in the compiler, the build and the release renamer exactly as in the loader, Begin followed by a tab is accepted, Enabled = False on popup-menu items is applied, properties after a nested child's End apply to the parent, and Layout on a control that is not a container is reported.
  • -
  • Include libraries: sql.bas, comm.bas and commdlg.bas take and return Long (SQLFieldInt and SQLAffected no longer wrap above 32767); resource.bas string functions are declared AS STRING. CommHandshake takes an optional passphrase, and the new CommSetPlain accepts unencrypted data on a channel.
  • -
  • commdlg.bas functions are renamed to match the other include libraries: basFileOpen is now DlgFileOpen$, basFileSave is DlgFileSave$, basInputBox2 is DlgInputBox$, basInputCancelled is DlgInputCancelled, basChoiceDialog is DlgChoice, basIntInput is DlgIntInput and basPromptSave is DlgPromptSave. The old names are gone; programs that use them must be updated and rebuilt.
  • +
  • .frm files: End followed by blanks or a comment (also End'comment) closes its block while End followed by a word does not, in the compiler, the build, and the release renamer exactly as in the loader, Begin followed by a tab is accepted, Enabled = False on popup-menu items is applied, properties after a nested child's End apply to the parent, and Layout on a control that is not a container is reported.
  • +
  • Include libraries: sql.bas, comm.bas, and commdlg.bas take and return Long (SQLFieldInt and SQLAffected no longer wrap above 32767); resource.bas string functions are declared AS STRING. CommHandshake takes an optional passphrase, and the new CommSetPlain accepts unencrypted data on a channel.
  • +
  • commdlg.bas functions are renamed to match the other include libraries: basFileOpen is now DlgFileOpen$, basFileSave is DlgFileSave$, basInputBox2 is DlgInputBox$, basInputCancelled is DlgInputCancelled, basChoiceDialog is DlgChoice, basIntInput is DlgIntInput, and basPromptSave is DlgPromptSave. The old names are gone; programs that use them must be updated and rebuilt.
  • New in comm.bas: CommRecvEncrypted(handle, channel) returns True when every byte the last CommRecv$ on that channel returned arrived encrypted and authenticated (also when it returned nothing), and False when any arrived unencrypted (before CommHandshake or on a CommSetPlain channel).
  • -
  • KeyDown: Shift, Ctrl or Alt with F1-F12, and Alt with Home, End, PgUp, PgDn, Ins, Del and the arrow keys, report the plain key's KeyCode with the modifier in Shift, even when the modifier is released before the program sees the key.
  • -
  • Data control: while its Validate handler runs, MoveFirst, MoveLast, MoveNext, MovePrevious, AddNew, Refresh and Update on the same Data control are ignored (they used to validate the same record again without end); Delete is allowed. Only unloading the Data control itself from Validate or Reposition stops the operation that fired it; removing other controls does not.
  • +
  • KeyDown: Shift, Ctrl, or Alt with F1-F12, and Alt with Home, End, PgUp, PgDn, Ins, Del, and the arrow keys, report the plain key's KeyCode with the modifier in Shift, even when the modifier is released before the program sees the key.
  • +
  • Data control: while its Validate handler runs, MoveFirst, MoveLast, MoveNext, MovePrevious, AddNew, Refresh, and Update on the same Data control are ignored (they used to validate the same record again without end); Delete is allowed. Only unloading the Data control itself from Validate or Reposition stops the operation that fired it; removing other controls does not.
  • Data control: writing a field back with the value it already has is not an edit, so leaving a bound control no longer fires Validate. With a SELECT query as RecordSource, edits are dropped when the current record changes. Update and Delete still find a record whose key is a number stored without a declared column type, or is NULL, when the key names exactly one record; Delete keeps the record shown when the database row could not be deleted.
  • -
  • DBGrid re-sorts and re-measures its columns when the Data control's records change (Refresh, AddNew, Delete or an edit), keeping the chosen sort column, and not on a plain record move.
  • +
  • DBGrid re-sorts and re-measures its columns when the Data control's records change (Refresh, AddNew, Delete, or an edit), keeping the chosen sort column, and not on a plain record move.
  • Image and ImageButton: Picture = "" clears the picture (Picture then reads back ""); a file that cannot be loaded leaves the picture and Picture unchanged. A disabled Image keeps its transparent color transparent.
  • Timer: a Timer whose handler shows a modal dialog no longer fires a second time from inside that dialog.
  • TextArea: the horizontal scroll bar appears as soon as the longest line fills the visible width, and the caret stays visible while typing at the right edge.
  • @@ -2227,7 +2288,7 @@ End Sub
  • TabControl: switching tabs closes an open drop-down list only when it belongs to a control on the page being hidden. A Label on a Toolbar is drawn on the toolbar's color.
  • Compile errors from the command-line compiler name the file and line. A .frm without a Begin Form line is an error. A release build that runs out of memory while renaming names fails instead of producing a broken program.
  • A form shown modal has no minimize gadget.
  • -
  • Menu items accept only Name (read-only), Checked and Enabled in code; Caption, Visible, RadioCheck, Shortcut and ShortcutText are design-time settings, and using them in code is a compile error in the IDE.
  • +
  • Menu items accept only Name (read-only), Checked, and Enabled in code; Caption, Visible, RadioCheck, Shortcut, and ShortcutText are design-time settings, and using them in code is a compile error in the IDE.
  • IDE

    @@ -2280,7 +2343,7 @@ End Sub ----------- ----- Control A control on a form (any type of control) Form A form - Object Any object: a form, a control, an instance of a class module or a Collection + Object Any object: a form, a control, an instance of a class module, or a Collection ClassName An instance of that class module (see Objects and Class Modules) Collection A Collection object (see Collection Object)
    Dim c As Control
    @@ -2289,7 +2352,7 @@ c.Text = "Hello"
     
     Dim people As New Collection
     people.Add "Ann"
    -

    An object variable that has not been Set holds Nothing. Control, Form and Object variables accept each other's values; a variable declared As a class name or As Collection accepts only an object of that kind, or Nothing: an object of another kind raises error 13 (Type mismatch), and a value that is not an object at all raises error 424 (Object required). The Source parameter of the DragDrop and DragOver events is a Control.

    +

    An object variable that has not been Set holds Nothing. Control, Form, and Object variables accept each other's values; a variable declared As a class name or As Collection accepts only an object of that kind, or Nothing: an object of another kind raises error 13 (Type mismatch), and a value that is not an object at all raises error 424 (Object required). The Source parameter of the DragDrop and DragOver events is a Control.

    See also: Objects and Class Modules

    Internal Types

    These types are not directly declarable but are used internally by the runtime.

    @@ -2297,8 +2360,8 @@ people.Add "Ann" ------------- ----------- Array Reference-counted multi-dimensional array (up to 8 dimensions) UDT User-defined type instance (created with TYPE...END TYPE) - Object Reference to a form, a control, a class instance or a Collection; declarable as Control, Form, Object, a class name or Collection - Ref Reference to a variable, array element, TYPE field or class member variable (what a ByRef parameter holds) + Object Reference to a form, a control, a class instance, or a Collection; declarable as Control, Form, Object, a class name, or Collection + Ref Reference to a variable, array element, TYPE field, or class member variable (what a ByRef parameter holds)

    Type Suffixes

    Type suffixes can be appended to variable names to declare their type implicitly:

    count%  = 42         ' Integer
    @@ -2323,10 +2386,10 @@ name$   = "Hello"    ' String
    Note: Both E and D can introduce an exponent (e.g. 1.5E10 and 2.5D3 are equivalent forms of a scientific-notation double).

    A literal with an Integer or Long suffix and a fraction or exponent is rounded (1E3% is 1000, 2.5% is 2). The most negative values -32768% and -2147483648& are valid. A trailing decimal point is allowed (1. is the Double 1). A numeric literal too long to read is a compile error.

    Type Promotion

    -

    When mixing types in expressions, values are automatically promoted to a common type: Byte -> Integer -> Long -> Single -> Double. A Currency with a Byte, Integer, Long or Currency stays Currency (see Byte and Currency below). Whole-number arithmetic keeps the type of its operands: an Integer result too large for an Integer becomes a Long (32767 + 1 is the Long 32768) and a Byte result outside 0 to 255 becomes an Integer, but a Long result too large for a Long raises error 6 (Overflow), as in Visual Basic. Convert an operand when a larger result is expected: CDBL(2147483647) + 1 is 2147483648.

    +

    When mixing types in expressions, values are automatically promoted to a common type: Byte -> Integer -> Long -> Single -> Double. A Currency with a Byte, Integer, Long, or Currency stays Currency (see Byte and Currency below). Whole-number arithmetic keeps the type of its operands: an Integer result too large for an Integer becomes a Long (32767 + 1 is the Long 32768) and a Byte result outside 0 to 255 becomes an Integer, but a Long result too large for a Long raises error 6 (Overflow), as in Visual Basic. Convert an operand when a larger result is expected: CDBL(2147483647) + 1 is 2147483648.

    + joins two strings. When a string meets a number in an arithmetic operator (including +) or a comparison, the string must hold a number, as in Visual Basic: "1" + 2 is 3 and " 2.5 " * 2 is 5, but text that is not a number ("abc" + 2, "12abc" + 2) raises error 13 (Type mismatch). VAL reads the number at the start of any text instead (VAL("12abc") is 12). Use & to join text and numbers, and VAL / STR$ to convert explicitly.

    Assignment Conversion

    -

    Storing a value into a variable declared as Byte, Integer, Long, Single, Double, Currency, String, Boolean or Date (by suffix, by AS clause, by DEFtype, or as a typed parameter, STATIC variable, FUNCTION result, TYPE field or array element) converts the value to that type: a number stored in a String variable becomes its text as STR$ gives it without the leading space ("5"). Text stored in a numeric variable must be a number, as in Visual Basic: "12" and " 3.5 " convert, while text that is not a number ("abc", "12abc") raises error 13 (Type mismatch) and the variable keeps its value. DEFtype statements also give parameters, STATIC variables and FUNCTION results their default type. Byte, Integer and Long round to the nearest whole number, with halves rounding to the nearest even number (2.5 becomes 2, 3.5 becomes 4), and raise error 6 (Overflow) when the result does not fit; Currency rounds to four decimal places the same way. Single keeps about 7 significant digits, so PRINT shows at most 7 digits of it. Boolean stores True for any non-zero number or non-empty string and False otherwise. Date takes a number as a date's number of days and reads a string as a date (error 13 when the text is not a date, error 6 outside 1/1/100 to 12/31/9999); INPUT into a Date variable reads the date the same way. A variable declared with DIM but without a type is Single (or the DEFtype for its first letter). A variable that is never declared keeps whatever type the assigned value has. A variable declared with a type holds that type from the start, before anything is stored in it: VARTYPE of a Long that was never assigned is 3 (vbLong) and of a String 8 (vbString).

    +

    Storing a value into a variable declared as Byte, Integer, Long, Single, Double, Currency, String, Boolean, or Date (by suffix, by AS clause, by DEFtype, or as a typed parameter, STATIC variable, FUNCTION result, TYPE field, or array element) converts the value to that type: a number stored in a String variable becomes its text as STR$ gives it without the leading space ("5"). Text stored in a numeric variable must be a number, as in Visual Basic: "12" and " 3.5 " convert, while text that is not a number ("abc", "12abc") raises error 13 (Type mismatch) and the variable keeps its value. DEFtype statements also give parameters, STATIC variables, and FUNCTION results their default type. Byte, Integer, and Long round to the nearest whole number, with halves rounding to the nearest even number (2.5 becomes 2, 3.5 becomes 4), and raise error 6 (Overflow) when the result does not fit; Currency rounds to four decimal places the same way. Single keeps about 7 significant digits, so PRINT shows at most 7 digits of it. Boolean stores True for any non-zero number or non-empty string and False otherwise. Date takes a number as a date's number of days and reads a string as a date (error 13 when the text is not a date, error 6 outside 1/1/100 to 12/31/9999); INPUT into a Date variable reads the date the same way. A variable declared with DIM but without a type is Single (or the DEFtype for its first letter). A variable that is never declared keeps whatever type the assigned value has. A variable declared with a type holds that type from the start, before anything is stored in it: VARTYPE of a Long that was never assigned is 3 (vbLong) and of a String 8 (vbString).

    Dim n As Integer
     n = 3.7            ' n is 4
     n = 40000          ' Error 6: Overflow
    @@ -2341,7 +2404,7 @@ x = 1 / 3          ' x was never declared and keeps the Double: prints 0.3333333
     

    Boolean values use -1 for True and 0 for False. Any non-zero numeric value is treated as True in a conditional context. The keywords True and False are reserved and may be used anywhere a Boolean value is expected.

    Byte and Currency

    A Byte holds a whole number from 0 to 255 in one byte. Storing a value outside that range raises error 6 (Overflow). Arithmetic on two Bytes gives a Byte when the result is 0 to 255 and an Integer otherwise; negating a Byte gives an Integer. CBYTE converts a value to a Byte.

    -

    A Currency holds an amount of money exactly: it counts ten-thousandths, so it has four decimal places and adding, subtracting or multiplying amounts never drifts (adding 0.1@ ten times gives exactly 1). A value with more decimal places is rounded to four, halves to the nearest even digit (CCUR(2.00005) is 2, CCUR(2.00015) is 2.0002), and a result outside the Currency range raises error 6 (Overflow). +, - and * on a Currency and a Byte, Integer, Long or Currency give a Currency; with a Single or Double they give a Double, and / always gives a Double. Comparisons of Currency values are exact. PRINT and STR$ show a Currency with only the decimals it needs (12.5); FORMAT$ lays it out like any other number. A FOR loop with a Currency counter steps exactly. CCUR converts a value to a Currency.

    +

    A Currency holds an amount of money exactly: it counts ten-thousandths, so it has four decimal places and adding, subtracting, or multiplying amounts never drifts (adding 0.1@ ten times gives exactly 1). A value with more decimal places is rounded to four, halves to the nearest even digit (CCUR(2.00005) is 2, CCUR(2.00015) is 2.0002), and a result outside the Currency range raises error 6 (Overflow). +, -, and * on a Currency and a Byte, Integer, Long, or Currency give a Currency; with a Single or Double they give a Double, and / always gives a Double. Comparisons of Currency values are exact. PRINT and STR$ show a Currency with only the decimals it needs (12.5); FORMAT$ lays it out like any other number. A FOR loop with a Currency counter steps exactly. CCUR converts a value to a Currency.

    Dim total As Currency
     Dim i As Integer
     For i = 1 To 3
    @@ -2359,7 +2422,7 @@ d = #2000-01-15 15:30#           ' year first, 24-hour time
     d = #Jan 15, 2000 3:30:00 PM#
     d = #3:30 PM#                    ' a time alone is on 12/30/1899

    Two-digit years 00 to 29 are 2000 to 2029 and 30 to 99 are 1930 to 1999. Date is not a reserved word, so a program may still use Date (or any of the date function names) for its own variables and procedures; the program's own name wins.

    -

    PRINT, STR$, CSTR and the & operator show a date as m/d/yyyy h:mm:ss AM (1/15/2000 3:30:00 PM). A date at midnight shows only the date and a time on 12/30/1899 only the time; a Date variable that was never assigned is 12:00:00 AM. Use FORMAT$ for any other layout.

    +

    PRINT, STR$, CSTR, and the & operator show a date as m/d/yyyy h:mm:ss AM (1/15/2000 3:30:00 PM). A date at midnight shows only the date and a time on 12/30/1899 only the time; a Date variable that was never assigned is 12:00:00 AM. Use FORMAT$ for any other layout.

    Adding a number of days to a date, or subtracting one, gives a date (fractions are parts of a day, so d + 0.25 is six hours later); subtracting one date from another gives the number of days between them as a Double. Comparisons put dates in time order, and a string compared with a date is read as a date:

    Dim due As Date
     due = Date + 30                     ' thirty days from today
    @@ -2372,7 +2435,7 @@ If due = "1/15/2026" Then Print "Match"

    Variants

    -

    A Variant is a variable that can hold any kind of value: a number, text, a date, True or False, an object or a whole array. It takes on the type of whatever is stored in it, and can change type each time something new is stored. A Variant can also hold two special values that no other type has: Empty (nothing has been stored yet) and Null (the value is unknown or missing).

    +

    A Variant is a variable that can hold any kind of value: a number, text, a date, True or False, an object, or a whole array. It takes on the type of whatever is stored in it, and can change type each time something new is stored. A Variant can also hold two special values that no other type has: Empty (nothing has been stored yet) and Null (the value is unknown or missing).

    Dim v As Variant
     Print TypeName(v)       ' Empty
     v = 42
    @@ -2384,7 +2447,7 @@ Print TypeName(v)       ' Date
     v = Null
     Print IsNull(v)         ' True

    Declare a Variant with AS VARIANT. Arrays of Variants (DIM list(10) AS VARIANT), Variant parameters, and FUNCTIONs and properties AS VARIANT work the same way; each element of a Variant array starts Empty. A variable declared with DIM but no AS clause, or never declared at all, is not a Variant: it keeps the older rules described under Assignment Conversion.

    -

    Storing a Variant in a variable of another type converts the value to that type, as for any other assignment. A Variant that is given an array gets its own copy of the array, as in Visual Basic: after v = list, changing list(1) does not change v(1), and w = v copies again. The same holds when a Variant holding an array is passed BYVAL, stored in an object's member or added to a Collection. Index it like the array itself, v(1).

    +

    Storing a Variant in a variable of another type converts the value to that type, as for any other assignment. A Variant that is given an array gets its own copy of the array, as in Visual Basic: after v = list, changing list(1) does not change v(1), and w = v copies again. The same holds when a Variant holding an array is passed BYVAL, stored in an object's member, or added to a Collection. Index it like the array itself, v(1).

    Dim list(2) As Integer, v As Variant
     list(0) = 1
     v = list                ' v holds a copy
    @@ -2397,20 +2460,20 @@ total = total + 5       ' Empty counts as 0: total is 5
     If IsEmpty(v) Then Print "Nothing stored yet"

    Null

    Null means "no valid value" -- for example a field that was never filled in. Null spreads through most expressions: anything calculated from a Null is Null too, so a missing value is not mistaken for a real one.

    -
      Expression                                    Result
    -  ----------                                    ------
    -  Null + 1, Null * x, -Null, Not Null           Null
    -  Null = 1, Null < x, Null = Null               Null (a comparison with Null is never True)
    -  Abs(Null), Int(Null), Fix(Null), Sgn(Null)    Null
    -  Left(Null, 1), UCase(Null), Len(Null), ...    Null (the Variant string functions; see below)
    -  "Name: " & Null                               "Name: " (& treats Null as "")
    -  Null & Null                                   Null
    -  False And Null                                False
    -  True Or Null                                  True
    -  False Imp Null, Null Imp True                 True
    -  Any other And, Or, Xor, Eqv or Imp with Null  Null
    -

    An IF, ELSEIF, WHILE, DO or LOOP condition that is Null counts as False. Any other use that needs a real number -- storing Null in a numeric, Boolean or Date variable, CINT(Null) and the other conversion functions, a string function ending in $ such as UCASE$ or LEFT$, or an array subscript -- raises error 94 (Invalid use of Null).

    -

    As in Visual Basic, the string functions without $ are Variant functions that pass Null through: LEFT, RIGHT, MID, UCASE, LCASE, TRIM, LTRIM, RTRIM, CHR, HEX, OCT, STR, SPACE and STRING give Null when their text (or number) argument is Null, and so do LEN, INSTR, INSTRREV, STRCOMP, LIKE and the date parts YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, WEEKDAY and DATEPART. Their $ forms (LEFT$, UCASE$ ...) always give text and raise error 94 for Null.

    +
      Expression                                     Result
    +  ----------                                     ------
    +  Null + 1, Null * x, -Null, Not Null            Null
    +  Null = 1, Null < x, Null = Null                Null (a comparison with Null is never True)
    +  Abs(Null), Int(Null), Fix(Null), Sgn(Null)     Null
    +  Left(Null, 1), UCase(Null), Len(Null), ...     Null (the Variant string functions; see below)
    +  "Name: " & Null                                "Name: " (& treats Null as "")
    +  Null & Null                                    Null
    +  False And Null                                 False
    +  True Or Null                                   True
    +  False Imp Null, Null Imp True                  True
    +  Any other And, Or, Xor, Eqv, or Imp with Null  Null
    +

    An IF, ELSEIF, WHILE, DO, or LOOP condition that is Null counts as False. Any other use that needs a real number -- storing Null in a numeric, Boolean, or Date variable, CINT(Null) and the other conversion functions, a string function ending in $ such as UCASE$ or LEFT$, or an array subscript -- raises error 94 (Invalid use of Null).

    +

    As in Visual Basic, the string functions without $ are Variant functions that pass Null through: LEFT, RIGHT, MID, UCASE, LCASE, TRIM, LTRIM, RTRIM, CHR, HEX, OCT, STR, SPACE, and STRING give Null when their text (or number) argument is Null, and so do LEN, INSTR, INSTRREV, STRCOMP, LIKE, and the date parts YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, WEEKDAY, and DATEPART. Their $ forms (LEFT$, UCASE$ ...) always give text and raise error 94 for Null.

    Because Null = Null is itself Null, test for Null with ISNULL, never with =:

    If IsNull(price) Then
         Print "No price yet"
    @@ -2421,11 +2484,11 @@ End If

    Variant Functions

      Function      Returns   Description
       --------      -------   -----------
    -  VARTYPE(v)    Integer   A number for the kind of value v holds (see VarType Constants): vbEmpty (0), vbNull (1), vbInteger (2), vbString (8), vbObject (9) and so on. For an array it is vbArray (8192) plus the element type, so an Integer array is 8194 and a Variant array 8204.
    +  VARTYPE(v)    Integer   A number for the kind of value v holds (see VarType Constants): vbEmpty (0), vbNull (1), vbInteger (2), vbString (8), vbObject (9), and so on. For an array it is vbArray (8192) plus the element type, so an Integer array is 8194 and a Variant array 8204.
       TYPENAME(v)   String    The name of the kind of value: "Byte", "Integer", "Long", "Single", "Double", "Currency", "String", "Boolean", "Date", "Empty", "Null", "Error" (a left-out OPTIONAL argument), the name of a TYPE, "Integer()" for an array, and for objects the class name, "Collection", "Nothing", or the type of a form or control.
       ISEMPTY(v)    Boolean   True when v is Empty.
       ISNULL(v)     Boolean   True when v is Null.
    -  ISNUMERIC(v)  Boolean   True when v is a number, True or False, Empty, or text that reads as a number ("12", "1.5"). False for Null, dates and other text.
    +  ISNUMERIC(v)  Boolean   True when v is a number, True or False, Empty, or text that reads as a number ("12", "1.5"). False for Null, dates, and other text.
       ISOBJECT(v)   Boolean   True when v is an object reference, including Nothing.
       ISARRAY(v)    Boolean   True when v is an array.
       ISMISSING(v)  Boolean   True when v is an OPTIONAL Variant parameter the caller left out (see Procedures).
    @@ -2496,7 +2559,7 @@ End Sub
    Like The text on the left matches the pattern on the right

    String comparisons are controlled by OPTION COMPARE. With OPTION COMPARE BINARY (the default) strings are compared by byte value. With OPTION COMPARE TEXT comparisons are case-insensitive. A comparison with Null gives Null (see Variants).

    Is compares object references, not values: a Is b is True when both refer to the very same object, and x Is Nothing tests for an object variable that holds no object. Either side may be a form or control name. Using Is on something that is not an object raises error 424 (Object required).

    -

    text Like pattern is True when the whole of text matches the pattern. In the pattern, ? stands for any one character, * for any run of characters (none too), # for one digit 0 to 9, [list] for one character in the list and [!list] for one character not in it; anything else stands for itself. A list holds characters and ranges such as a-z ([A-Za-z0-9_]); to match a literal ?, *, # or [, put it in brackets ([*]). OPTION COMPARE TEXT makes the match ignore case. Like with Null gives Null, and a pattern with a [ that has no ] raises error 93.

    +

    text Like pattern is True when the whole of text matches the pattern. In the pattern, ? stands for any one character, * for any run of characters (none too), # for one digit 0 to 9, [list] for one character in the list, and [!list] for one character not in it; anything else stands for itself. A list holds characters and ranges such as a-z ([A-Za-z0-9_]); to match a literal ?, *, #, or [, put it in brackets ([*]). OPTION COMPARE TEXT makes the match ignore case. Like with Null gives Null, and a pattern with a [ that has no ] raises error 93.

    If name$ Like "A*" Then Print "Starts with A"
     If code$ Like "[A-Z]##" Then Print "A letter and two digits"
     If file$ Like "*.[!b]*" Then Print "Not a .b... extension"
    @@ -2540,7 +2603,7 @@ REM This is a comment

    Flow Control (EXIT, CALL, GOTO, GOSUB, ON)

    Input/Output (PRINT, INPUT, DATA/READ)

    Miscellaneous Statements (ON ERROR, SHELL, SLEEP, END)

    -

    Sound, Joysticks and Frames (BEEP, SOUND, PLAY, STICK, STRIG, FRAMEWAIT)

    +

    Sound, Joysticks, and Frames (BEEP, SOUND, PLAY, STICK, STRIG, FRAMEWAIT)

    Objects and Class Modules (NEW, PROPERTY, Class_Initialize, Me)

    Collection Object (Add, Remove, Item, Count)

    @@ -2570,7 +2633,7 @@ Dim matrix(1 To 10, 1 To 10) As Single Dim Shared globalFlag As Boolean Dim record As PersonType Dim fixedStr As String * 20 -
    Note: A module-level variable is visible inside a SUB or FUNCTION only when it is declared with DIM SHARED (or named in a SHARED statement in that procedure). See Variable Scope below. Inside a SUB or FUNCTION, DIM declares a local variable that is recreated on each call (use STATIC to retain its value between calls). A SUB or FUNCTION holds at most 64 parameters and local variables together, including a few hidden ones that statements such as ON ... GOTO, SWAP and READ into array elements use while they run; a procedure that needs more fails to compile with "Too many local variables". A program holds at most 512 module-level variables, a form at most 65536 form-level variables and a class at most 65536 member variables; more fails to compile.
    +
    Note: A module-level variable is visible inside a SUB or FUNCTION only when it is declared with DIM SHARED (or named in a SHARED statement in that procedure). See Variable Scope below. Inside a SUB or FUNCTION, DIM declares a local variable that is recreated on each call (use STATIC to retain its value between calls). A SUB or FUNCTION holds at most 64 parameters and local variables together, including a few hidden ones that statements such as ON ... GOTO, SWAP, and READ into array elements use while they run; a procedure that needs more fails to compile with "Too many local variables". A program holds at most 512 module-level variables, a form at most 65536 form-level variables, and a class at most 65536 member variables; more fails to compile.

    Fixed-length strings (STRING * n) are padded with spaces and truncated when assigned so their length is always exactly n.

    DIM variable AS NEW ClassName (or AS NEW Collection) creates a new object at the DIM and stores it in the variable; without NEW an object variable starts as Nothing. See Objects and Class Modules. DIM variable AS NEW FormName makes a new copy of a form of the project (see Several Copies of a Form in the Form topic); DIM variable AS FormName declares a variable for that form or a copy of it.

    Variable Scope and SHARED

    @@ -2580,7 +2643,7 @@ Dim fixedStr As String * 20
  • A SUB or FUNCTION sees a module variable only when it was declared with DIM SHARED at module level, or when the procedure names it in a SHARED statement. This applies to arrays as well.
  • Any other name used inside a SUB or FUNCTION is a local variable of that procedure, even when a module variable of the same name exists. With OPTION EXPLICIT, using such a name without a DIM is a compile error that says the module variable is not SHARED.
  • Indexing (name(...)) or dotting (name.member) a module variable the procedure cannot see is a compile error.
  • -
  • CONSTs declared at module level, TYPEs, SUBs, FUNCTIONs, class modules, forms and controls are visible everywhere; no SHARED is needed for them. PUBLIC at module level is the same as DIM SHARED (see PUBLIC / PRIVATE).
  • +
  • CONSTs declared at module level, TYPEs, SUBs, FUNCTIONs, class modules, forms, and controls are visible everywhere; no SHARED is needed for them. PUBLIC at module level is the same as DIM SHARED (see PUBLIC / PRIVATE).
  • A DEF FN is module-level code: its expression sees every module variable.
  • DIM SHARED total AS LONG       ' visible in every SUB and FUNCTION
    @@ -2601,12 +2664,12 @@ END SUB

    How Names Are Found

    Every name in a program is looked up the same way, in this order:

    -

    A SUB or FUNCTION can be called before its definition, and a TYPE, ENUM or class -- with its member variables and procedures -- can be used before it is declared: the compiler reads every declaration first. A name after a dot is a member of the object before it: when that object's class is known (see Objects and Class Modules) the member is looked up in the class when the program is compiled, else when it runs.

    +

    A SUB or FUNCTION can be called before its definition, and a TYPE, ENUM, or class -- with its member variables and procedures -- can be used before it is declared: the compiler reads every declaration first. A name after a dot is a member of the object before it: when that object's class is known (see Objects and Class Modules) the member is looked up in the class when the program is compiled, else when it runs.

    PUBLIC / PRIVATE

    At module level, PUBLIC and PRIVATE can be used in place of DIM, as in Visual Basic. PUBLIC declares a variable that every SUB and FUNCTION can use (the same as DIM SHARED); PRIVATE declares one that only module-level code sees (the same as DIM).

    Public highScore As Long        ' same as Dim Shared highScore As Long
    @@ -2641,12 +2704,12 @@ Const BIG = MAX_SIZE > 50
  • literals (numbers, strings, True and False, #date# literals);
  • constants declared earlier, including ENUM members (Member or EnumName.Member);
  • the arithmetic operators + - * / \ MOD ^, the comparison operators, AND OR NOT XOR EQV IMP, and string joining with & or +;
  • -
  • these built-in functions: ABS, ASC, ATN, CBOOL, CDATE, CDBL, CHOOSE, CHR$, CINT, CLNG, COS, CSNG, CSTR, DATEADD, DATEDIFF, DATEPART, DATESERIAL, DATEVALUE, DAY, EXP, FIX, FORMAT$, GETBLUE, GETGREEN, GETRED, HEX$, HOUR, IIF, INSTR, INSTRREV, INT, ISDATE, LCASE$, LEFT$, LEN, LOG, LTRIM$, MID$, MINUTE, MONTH, MONTHNAME, OCT$, REPLACE$, RGB, RIGHT$, RTRIM$, SECOND, SGN, SIN, SPACE$, SQR, STR$, STRCOMP, STRING$, STRREVERSE$, TAN, TIMESERIAL, TIMEVALUE, TRIM$, UCASE$, VAL, WEEKDAY, WEEKDAYNAME and YEAR.
  • +
  • these built-in functions: ABS, ASC, ATN, CBOOL, CDATE, CDBL, CHOOSE, CHR$, CINT, CLNG, COS, CSNG, CSTR, DATEADD, DATEDIFF, DATEPART, DATESERIAL, DATEVALUE, DAY, EXP, FIX, FORMAT$, GETBLUE, GETGREEN, GETRED, HEX$, HOUR, IIF, INSTR, INSTRREV, INT, ISDATE, LCASE$, LEFT$, LEN, LOG, LTRIM$, MID$, MINUTE, MONTH, MONTHNAME, OCT$, REPLACE$, RGB, RIGHT$, RTRIM$, SECOND, SGN, SIN, SPACE$, SQR, STR$, STRCOMP, STRING$, STRREVERSE$, TAN, TIMESERIAL, TIMEVALUE, TRIM$, UCASE$, VAL, WEEKDAY, WEEKDAYNAME, and YEAR.
  • -

    Anything else -- a variable, a FUNCTION of your own, or a built-in whose answer can change from run to run such as TIMER, NOW, RND or DIR$ -- is a compile error ("Constant expression required"). An expression that fails, such as a division by zero or a result too big for its type, is also reported when the program is compiled. Text comparisons in a constant follow the OPTION COMPARE setting in effect where the constant is declared. A string constant can be at most 255 characters long.

    -

    Without an AS clause the constant takes the type of its value: a whole number makes a Long constant, a number with a fraction a Double constant, and strings, Booleans and dates keep their type. With an AS clause the value is converted to that type: Integer and Long values are rounded and must fit the type, and a string for a numeric type (or a number for String) is a compile error.

    +

    Anything else -- a variable, a FUNCTION of your own, or a built-in whose answer can change from run to run such as TIMER, NOW, RND, or DIR$ -- is a compile error ("Constant expression required"). An expression that fails, such as a division by zero or a result too big for its type, is also reported when the program is compiled. Text comparisons in a constant follow the OPTION COMPARE setting in effect where the constant is declared. A string constant can be at most 255 characters long.

    +

    Without an AS clause the constant takes the type of its value: a whole number makes a Long constant, a number with a fraction a Double constant, and strings, Booleans, and dates keep their type. With an AS clause the value is converted to that type: Integer and Long values are rounded and must fit the type, and a string for a numeric type (or a number for String) is a compile error.

    Constants declared at module level are visible to all procedures. A CONST declared inside a SUB or FUNCTION is local to that procedure; it may reuse the name of a module constant without changing it.

    -

    PUBLIC CONST and PRIVATE CONST work as in Visual Basic. A Private constant, and any constant of a form or class module, belongs to the file it is declared in: another module, form or class of the project can declare its own constant of the same name, and does not see this one. A Public constant, or a plain CONST in a module, is seen by the whole program. A class module or form cannot have a Public constant ("Constants are not allowed as Public members of a class module or form").

    +

    PUBLIC CONST and PRIVATE CONST work as in Visual Basic. A Private constant, and any constant of a form or class module, belongs to the file it is declared in: another module, form, or class of the project can declare its own constant of the same name, and does not see this one. A Public constant, or a plain CONST in a module, is seen by the whole program. A class module or form cannot have a Public constant ("Constants are not allowed as Public members of a class module or form").

    Public Const APP_VERSION = "2.1"   ' every file of the project
     Private Const BUFFER_SIZE = 512    ' this file only

    TYPE...END TYPE

    @@ -2665,7 +2728,7 @@ End Type Dim p As PersonType p.firstName = "Scott" p.age = 30 -

    UDT fields can themselves be UDTs (nested types), and field access chains through array elements too: shapes(i).origin.x = 5. A new instance starts with numeric fields at 0, Boolean fields False, variable strings empty and STRING * n fields padded to n spaces. TYPE variables can be written to and read from RANDOM and BINARY files with PUT and GET (see GET / PUT).

    +

    UDT fields can themselves be UDTs (nested types), and field access chains through array elements too: shapes(i).origin.x = 5. A new instance starts with numeric fields at 0, Boolean fields False, variable strings empty, and STRING * n fields padded to n spaces. TYPE variables can be written to and read from RANDOM and BINARY files with PUT and GET (see GET / PUT).

    ENUM...END ENUM

    Defines a set of named whole-number constants, and a type name for variables that hold one of them.

    ENUM EnumName
    @@ -2691,7 +2754,7 @@ Dim heading As Direction
     heading = West
     If heading = Direction.West Then Print "Going west"

    The first member is 0 and each member without a value is one more than the member before it. A value may be any constant expression (see CONST), including members declared earlier in the same ENUM or in another one. Members are Long constants and can be used on their own (West) or with the ENUM name in front (Direction.West). If two ENUMs have a member with the same name, that name must be written with its ENUM name.

    -

    The ENUM name can be used as a type wherever a type is allowed: DIM, parameters, FUNCTION results and TYPE fields. Such a variable is a Long, so it can hold any whole number, not only the members. ENUMs are declared at module level (not inside a SUB or FUNCTION) and are visible everywhere in the program; declare an ENUM before the code that uses its members. PRIVATE ENUM keeps the ENUM and its members to the file it is declared in, like PRIVATE CONST; PUBLIC ENUM (the same as a plain ENUM) is seen everywhere. Members are constants: Direction.West = 3 is a compile error ("Cannot assign to a constant").

    +

    The ENUM name can be used as a type wherever a type is allowed: DIM, parameters, FUNCTION results, and TYPE fields. Such a variable is a Long, so it can hold any whole number, not only the members. ENUMs are declared at module level (not inside a SUB or FUNCTION) and are visible everywhere in the program; declare an ENUM before the code that uses its members. PRIVATE ENUM keeps the ENUM and its members to the file it is declared in, like PRIVATE CONST; PUBLIC ENUM (the same as a plain ENUM) is seen everywhere. Members are constants: Direction.West = 3 is a compile error ("Cannot assign to a constant").

    DECLARE

    Forward-declares a SUB or FUNCTION. This is rarely required because the compiler supports forward references within a module, but it is still accepted for compatibility:

    DECLARE SUB name ([BYVAL] [OPTIONAL] param AS type, ...)
    @@ -2708,7 +2771,7 @@ END DECLARE
    Declare Sub SQLClose(ByVal db As Long) End Declare

    Arguments are always passed by value. Each argument is converted to the type its parameter is declared with before the call, so a Double passed to a Long parameter arrives as a whole number and a Single parameter receives a Single. Declare Integer only for native values that fit in 16 bits; use Long for 32-bit values, or large results are cut down to 16 bits. A Byte parameter or result is 8 bits. A Currency parameter receives the amount as a 64-bit whole number of ten-thousandths; a DECLARE FUNCTION cannot return a Currency.

    -

    A String parameter or result declared AS STRING is text: it ends at the first CHR$(0). To pass or receive any bytes, CHR$(0) included (network data, sound samples, encrypted data, checksums of binary data), the declaration says BINARY STRING instead. To the program it is an ordinary String; LEN, MID$, ASC and the other string functions see every byte. BINARY STRING is allowed only in a DECLARE LIBRARY block, and the shipped include files already use it where a function works with bytes.

    +

    A String parameter or result declared AS STRING is text: it ends at the first CHR$(0). To pass or receive any bytes, CHR$(0) included (network data, sound samples, encrypted data, checksums of binary data), the declaration says BINARY STRING instead. To the program it is an ordinary String; LEN, MID$, ASC, and the other string functions see every byte. BINARY STRING is allowed only in a DECLARE LIBRARY block, and the shipped include files already use it where a function works with bytes.

    Declare Library "mylib"
         Declare Function MyChecksum(ByVal bytes As Binary String) As Long
         Declare Function MyRead$(ByVal handle As Long, ByVal maxLen As Long) As Binary String
    @@ -2725,7 +2788,7 @@ Print MyChecksum(packet$)
    count = count + 1 Print count End Sub -

    Several variables can be declared in one STATIC statement, separated by commas, each with its own AS clause; STRING * n works as in DIM. Arrays and TYPE variables can be STATIC too and keep their contents between calls: a fixed-size array (Static hits(1 To 10) As Long) or a dynamic one sized later with REDIM (Static names() As String). In a class module each object has its own copy of a procedure's STATIC variables, arrays and TYPE variables, as in Visual Basic: two objects counting calls count separately. A STATIC AS NEW object or WithEvents variable is not supported (a compile error); use a module-level or member variable for those.

    +

    Several variables can be declared in one STATIC statement, separated by commas, each with its own AS clause; STRING * n works as in DIM. Arrays and TYPE variables can be STATIC too and keep their contents between calls: a fixed-size array (Static hits(1 To 10) As Long) or a dynamic one sized later with REDIM (Static names() As String). In a class module each object has its own copy of a procedure's STATIC variables, arrays, and TYPE variables, as in Visual Basic: two objects counting calls count separately. A STATIC AS NEW object or WithEvents variable is not supported (a compile error); use a module-level or member variable for those.

    Sub Remember(ByVal word As String)
         Static seen(1 To 3) As String
         Static count As Integer
    @@ -2765,7 +2828,7 @@ array(index) = expression
     udt.field = expression
     LET variable = expression

    The LET keyword is optional and supported for compatibility.

    -

    Assigning a whole TYPE value to a TYPE variable, array element or field copies the value's fields into the record that is already there, as in Visual Basic: the two stay separate records (changing one later does not change the other), and a SUB that was passed one of the target's fields by reference sees the new value.

    +

    Assigning a whole TYPE value to a TYPE variable, array element, or field copies the value's fields into the record that is already there, as in Visual Basic: the two stay separate records (changing one later does not change the other), and a SUB that was passed one of the target's fields by reference sees the new value.

    SWAP

    Exchanges the values of two variables or array elements. The two operands should be the same type.

    SWAP variable1, variable2
    @@ -2793,7 +2856,7 @@ Dim h As Header, r As Raw
     r.bytes = "AB" + Chr$(10) + Chr$(0)
     LSet h = r              ' h.kind = "AB", h.size = 10

    SET

    -

    Stores an object reference -- a form, a control, an instance of a class module, a Collection, or Nothing -- in a variable, array element, TYPE field or property. After SET both names refer to the same object; nothing is copied.

    +

    Stores an object reference -- a form, a control, an instance of a class module, a Collection, or Nothing -- in a variable, array element, TYPE field, or property. After SET both names refer to the same object; nothing is copied.

    SET variable = objectExpression
     SET variable = NEW ClassName
     SET variable = NEW FormName
    @@ -2815,7 +2878,7 @@ Set w = New Form1              ' a second Form1 window
     w.Show

    The value must be an object or Nothing; anything else raises error 424 (Object required). SET is not used for ordinary numeric or string assignment.

    ERASE

    -

    Frees the memory of a dynamic array (declared with DIM name(), created by REDIM, or an array parameter) and resets it to the undimensioned state; using it again before a REDIM raises error 13. A fixed array (declared with bounds) keeps its bounds and has every element reset to zero, an empty string, False or a fresh TYPE instance.

    +

    Frees the memory of a dynamic array (declared with DIM name(), created by REDIM, or an array parameter) and resets it to the undimensioned state; using it again before a REDIM raises error 13. A fixed array (declared with bounds) keeps its bounds and has every element reset to zero, an empty string, False, or a fresh TYPE instance.

    ERASE arrayName
     ERASE array1, array2, ...

    One ERASE can name several arrays, separated by commas.

    @@ -2950,13 +3013,13 @@ END SUB
    Sub Greet(ByVal name As String)
         Print "Hello, " & name
     End Sub
    -

    A SUB, FUNCTION or Property procedure stands on its own in the program: defining one inside another procedure, or inside an IF, FOR, DO, WHILE, SELECT CASE or WITH block, is a compile error.

    -

    Parameters are passed by reference by default (nested calls pass the same variable on, so a SUB called from a SUB can still change the original); BYREF may be written out to say so, and BYVAL selects value semantics. A by-reference parameter is the caller's variable itself, as in Visual Basic: a write to it changes the variable at once (the caller's code, or another parameter naming the same variable, sees the new value straight away, even if the procedure later stops with an error), and what is stored is converted to the variable's own type (a STRING * n keeps its length). This holds for a plain variable, an array element (`bump a(3)` changes `a(3)` in place), a TYPE field (`bump rec.count`, `bump recs(2).count`, `bump rec.inner.count`) and, inside a class, one of the class's own member variables. While an array element (or a field of one) is passed by reference, its array cannot be given a new size or erased: REDIM of that array, or ERASE of it when it is a dynamic array, raises error 10 (This array is fixed or temporarily locked), as in Visual Basic (ERASE of a fixed-size array only clears its elements and is allowed). The array is free again as soon as the call returns. Assigning a whole TYPE value to the variable (or element) whose field is being passed copies the value into the same record, so the parameter sees the new value (as in Visual Basic). A parameter written as name() receives a whole array by reference; the caller passes `arr()` (or just `arr`), and the SUB may index, REDIM or ERASE it. Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:

    +

    A SUB, FUNCTION, or Property procedure stands on its own in the program: defining one inside another procedure, or inside an IF, FOR, DO, WHILE, SELECT CASE, or WITH block, is a compile error.

    +

    Parameters are passed by reference by default (nested calls pass the same variable on, so a SUB called from a SUB can still change the original); BYREF may be written out to say so, and BYVAL selects value semantics. A by-reference parameter is the caller's variable itself, as in Visual Basic: a write to it changes the variable at once (the caller's code, or another parameter naming the same variable, sees the new value straight away, even if the procedure later stops with an error), and what is stored is converted to the variable's own type (a STRING * n keeps its length). This holds for a plain variable, an array element (`bump a(3)` changes `a(3)` in place), a TYPE field (`bump rec.count`, `bump recs(2).count`, `bump rec.inner.count`) and, inside a class, one of the class's own member variables. While an array element (or a field of one) is passed by reference, its array cannot be given a new size or erased: REDIM of that array, or ERASE of it when it is a dynamic array, raises error 10 (This array is fixed or temporarily locked), as in Visual Basic (ERASE of a fixed-size array only clears its elements and is allowed). The array is free again as soon as the call returns. Assigning a whole TYPE value to the variable (or element) whose field is being passed copies the value into the same record, so the parameter sees the new value (as in Visual Basic). A parameter written as name() receives a whole array by reference; the caller passes `arr()` (or just `arr`), and the SUB may index, REDIM, or ERASE it. Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:

    Greet "World"
     Greet("World")
     Call Greet("World")

    As in Visual Basic and QBasic, a single argument in parentheses after the name of a SUB called without CALL (Bump (n), or Bump(n)) is an expression in parentheses, so it is passed by value: the SUB gets a copy and n is not changed. Write Bump n, or Call Bump(n), to pass n by reference.

    -

    An argument is converted to the type of a typed parameter (one declared with AS, a type suffix or a DEFtype rule). A value -- a literal, an expression, a variable in its own parentheses, or anything given to a BYVAL parameter -- is converted the way an assignment would convert it (Report 3.7 gives an Integer parameter 4). A variable, array element or TYPE field given to a by-reference parameter must have the parameter's own type, as in Visual Basic: an Integer parameter cannot be the caller's Double variable ("ByRef argument type mismatch" when the program is compiled); pass CINT(d), or (d) in its own parentheses, to pass a converted copy. A variable declared without a type (a Variant) holds any value and is passed as it is. Through an object variable declared As Object the procedure is only found when the program runs, so a variable of another type is passed as a converted copy instead.

    +

    An argument is converted to the type of a typed parameter (one declared with AS, a type suffix, or a DEFtype rule). A value -- a literal, an expression, a variable in its own parentheses, or anything given to a BYVAL parameter -- is converted the way an assignment would convert it (Report 3.7 gives an Integer parameter 4). A variable, array element, or TYPE field given to a by-reference parameter must have the parameter's own type, as in Visual Basic: an Integer parameter cannot be the caller's Double variable ("ByRef argument type mismatch" when the program is compiled); pass CINT(d), or (d) in its own parentheses, to pass a converted copy. A variable declared without a type (a Variant) holds any value and is passed as it is. Through an object variable declared As Object the procedure is only found when the program runs, so a variable of another type is passed as a converted copy instead.

    Sub Fill(v() As Integer, ByRef count As Integer)
         ReDim v(9) As Integer
         v(9) = 7
    @@ -2978,7 +3041,7 @@ End Sub
     
     Announce "hello"            ' loud defaults to 0
     Announce "hello", True      ' loud is -1
    -

    An OPTIONAL parameter can also name the value it takes when it is left out, after an equals sign: Optional count As Integer = 10. The default is a constant expression, like the value of a CONST: numbers, strings, dates, other constants and the built-in functions CONST allows, converted to the parameter's type (a value that does not fit is a compile error). A parameter with a default is never Missing -- ISMISSING is False for it -- whether it is left out at the end, skipped between commas, or the procedure is reached through an object. Defaults work for SUB and FUNCTION procedures and for the procedures and Property procedures of class modules, including calls written before the procedure itself. As in Visual Basic the default is the one the declaration being called gives: a call through an interface (a variable declared As the interface class) passes the interface's default, even when the implementing class declares another. An object parameter (As Object, As Control, As Form or As a class) can only have the default Nothing, as in Visual Basic: Optional target As Object = Nothing; a Variant parameter may have Nothing too. A default cannot be given to a required parameter, an array or a TYPE, or in DEF FN and DECLARE LIBRARY. A DECLARE SUB / FUNCTION line must say whether each parameter has a default, as its definition does.

    +

    An OPTIONAL parameter can also name the value it takes when it is left out, after an equals sign: Optional count As Integer = 10. The default is a constant expression, like the value of a CONST: numbers, strings, dates, other constants, and the built-in functions CONST allows, converted to the parameter's type (a value that does not fit is a compile error). A parameter with a default is never Missing -- ISMISSING is False for it -- whether it is left out at the end, skipped between commas, or the procedure is reached through an object. Defaults work for SUB and FUNCTION procedures and for the procedures and Property procedures of class modules, including calls written before the procedure itself. As in Visual Basic the default is the one the declaration being called gives: a call through an interface (a variable declared As the interface class) passes the interface's default, even when the implementing class declares another. An object parameter (As Object, As Control, As Form, or As a class) can only have the default Nothing, as in Visual Basic: Optional target As Object = Nothing; a Variant parameter may have Nothing too. A default cannot be given to a required parameter, an array, or a TYPE, or in DEF FN and DECLARE LIBRARY. A DECLARE SUB / FUNCTION line must say whether each parameter has a default, as its definition does.

    Const SIDE = 10
     
     Function Area(Optional w As Integer = SIDE, Optional h As Integer = SIDE * 2) As Long
    @@ -3009,7 +3072,7 @@ Report "Sales", footer:="(end)"          ' width is 40
     Report width:=60, title:="Stock"
     Print Mid$(String:="hello", Start:=2)    ' ello
     MsgBox Prompt:="Saved", Title:="Notes"
    -

    Named arguments work for SUB and FUNCTION procedures, DEF FN functions, DECLARE LIBRARY procedures and the procedures of class modules (through a variable of the class, or through an Object or Variant variable), and for the built-in functions that Visual Basic gives argument names to: MsgBox (Prompt, Buttons, Title), InputBox (Prompt, Title, Default), Mid (String, Start, Length), Left and Right (String, Length), InStr (Start, String1, String2), InStrRev (StringCheck, StringMatch, Start, Compare), Replace (Expression, Find, Replace, Start, Count, Compare), Split (Expression, Delimiter, Limit, Compare), Join (SourceArray, Delimiter), Filter (SourceArray, Match, Include, Compare), StrComp (String1, String2, Compare), String (Number, Character), Format (Expression, Format), IIf (Expression, TruePart, FalsePart), DateAdd (Interval, Number, Date), DateDiff and DatePart (Interval, Date1 and Date2 or Date, FirstDayOfWeek, FirstWeekOfYear), DateSerial (Year, Month, Day), TimeSerial (Hour, Minute, Second), Weekday (Date, FirstDayOfWeek), WeekdayName (Weekday, Abbreviate, FirstDayOfWeek), MonthName (Month, Abbreviate), and a Collection's Add (Item, Key, Before, After), Item and Remove (Index).

    +

    Named arguments work for SUB and FUNCTION procedures, DEF FN functions, DECLARE LIBRARY procedures, and the procedures of class modules (through a variable of the class, or through an Object or Variant variable), and for the built-in functions that Visual Basic gives argument names to: MsgBox (Prompt, Buttons, Title), InputBox (Prompt, Title, Default), Mid (String, Start, Length), Left and Right (String, Length), InStr (Start, String1, String2), InStrRev (StringCheck, StringMatch, Start, Compare), Replace (Expression, Find, Replace, Start, Count, Compare), Split (Expression, Delimiter, Limit, Compare), Join (SourceArray, Delimiter), Filter (SourceArray, Match, Include, Compare), StrComp (String1, String2, Compare), String (Number, Character), Format (Expression, Format), IIf (Expression, TruePart, FalsePart), DateAdd (Interval, Number, Date), DateDiff and DatePart (Interval, Date1 and Date2 or Date, FirstDayOfWeek, FirstWeekOfYear), DateSerial (Year, Month, Day), TimeSerial (Hour, Minute, Second), Weekday (Date, FirstDayOfWeek), WeekdayName (Weekday, Abbreviate, FirstDayOfWeek), MonthName (Month, Abbreviate), and a Collection's Add (Item, Key, Before, After), Item, and Remove (Index).

    A name the procedure does not have is a compile error (Named argument not found), and so is a parameter given twice. When the procedure is only found as the program runs (a method called through an Object or Variant variable) the same mistakes raise error 448 (Named argument not found) and error 450 (Wrong number of arguments). The methods of forms and controls do not take named arguments (error 446, Object doesn't support named arguments), and neither does a ParamArray parameter.

    ParamArray

    The last parameter of a SUB or FUNCTION can be marked PARAMARRAY to accept any number of extra arguments. It is written as an array, name(), and receives every remaining argument as an array numbered from 0. With no extra arguments the array is empty: UBOUND returns -1.

    @@ -3033,7 +3096,7 @@ Function Sum(ParamArray n() As Double) As Double Next Sum = total End Function -

    Without an AS clause (and no type suffix or DEFtype rule for the name) the ParamArray is an array of Variants (TYPENAME gives "Variant()"): each element holds the argument that was passed with its own type, so numbers, strings and objects can be mixed, and FOR EACH can walk it. With an AS clause every argument is converted to that type. An argument that is a variable, a whole array, an array element (a(i), m(i, j)), a TYPE field (rec.f, arr(i).f, rec.inner.f) or, inside a class, one of the class's own member variables is passed by reference, as in Visual Basic: the element of the ParamArray is that variable, so a write to it changes the variable at once, converted to the variable's type (a value that cannot be converted raises its error at the assignment), and passing the element on (Bump v(0)) passes the variable. With an AS clause an argument is passed by reference only when its own type is that type; others are converted and passed by value. The same holds for a class's methods, whether the object variable is declared as the class or As Object / As Variant. While the call runs, the arrays whose elements were given are locked: REDIM of one of them (or ERASE of a dynamic one) raises error 10, as for any array element passed by reference. Copying the ParamArray (keep = v) copies the values, not the variables. Expressions and object properties are passed by value. Leaving an argument out (Sum 1, , 3) gives a Missing element. A PARAMARRAY cannot be combined with OPTIONAL parameters, cannot be BYVAL or BYREF, cannot hold a TYPE, and takes at most 31 arguments. It is not available in DEF FN or DECLARE LIBRARY.

    +

    Without an AS clause (and no type suffix or DEFtype rule for the name) the ParamArray is an array of Variants (TYPENAME gives "Variant()"): each element holds the argument that was passed with its own type, so numbers, strings, and objects can be mixed, and FOR EACH can walk it. With an AS clause every argument is converted to that type. An argument that is a variable, a whole array, an array element (a(i), m(i, j)), a TYPE field (rec.f, arr(i).f, rec.inner.f) or, inside a class, one of the class's own member variables is passed by reference, as in Visual Basic: the element of the ParamArray is that variable, so a write to it changes the variable at once, converted to the variable's type (a value that cannot be converted raises its error at the assignment), and passing the element on (Bump v(0)) passes the variable. With an AS clause an argument is passed by reference only when its own type is that type; others are converted and passed by value. The same holds for a class's methods, whether the object variable is declared as the class or As Object / As Variant. While the call runs, the arrays whose elements were given are locked: REDIM of one of them (or ERASE of a dynamic one) raises error 10, as for any array element passed by reference. Copying the ParamArray (keep = v) copies the values, not the variables. Expressions and object properties are passed by value. Leaving an argument out (Sum 1, , 3) gives a Missing element. A PARAMARRAY cannot be combined with OPTIONAL parameters, cannot be BYVAL or BYREF, cannot hold a TYPE, and takes at most 31 arguments. It is not available in DEF FN or DECLARE LIBRARY.

    FUNCTION...END FUNCTION

    Defines a function with a return value.

    FUNCTION name ([BYVAL] [OPTIONAL] param AS type, ...) AS returnType [STATIC]
    @@ -3044,7 +3107,7 @@ END FUNCTION
    Function Square(ByVal n As Double) As Double
         Square = n * n
     End Function
    -

    Assign to the function name to set the return value. The value is converted to the function's type (AS clause, type suffix or DEFtype), so FUNCTION F AS INTEGER with F = 2.5 returns 2. Inside the function its name on its own reads the value set so far (Total = Total + n), as in Visual Basic; with an argument list -- F() or F(n - 1) -- it calls the function again. Use EXIT FUNCTION to return early.

    +

    Assign to the function name to set the return value. The value is converted to the function's type (AS clause, type suffix, or DEFtype), so FUNCTION F AS INTEGER with F = 2.5 returns 2. Inside the function its name on its own reads the value set so far (Total = Total + n), as in Visual Basic; with an argument list -- F() or F(n - 1) -- it calls the function again. Use EXIT FUNCTION to return early.

    A FUNCTION can also be called as a statement, with or without parentheses; its result is discarded:

    Function Beep2(ByVal n As Integer) As Integer
         Beep2 = n * 2
    @@ -3052,7 +3115,7 @@ End Function
     
     Beep2 3              ' result discarded

    name(...) in an expression calls a FUNCTION; using a SUB there is a compile error.

    -

    Labels, GOSUB and Variables in Procedures

    +

    Labels, GOSUB, and Variables in Procedures

    Labels belong to the SUB or FUNCTION they are written in (or to the module level). GOTO, GOSUB, ON ... GOTO/GOSUB and ON ERROR GOTO can only reach a label of the same procedure, and different procedures can reuse the same label name. GOSUB and RETURN work inside a procedure; see GOTO / GOSUB / RETURN. A procedure sees module variables only when they are SHARED; see Variable Scope and SHARED.

    DEF FN

    Defines a single-expression function. The function name begins with FN and is invoked just like a normal function. DEF FN functions cannot span multiple statements. A DEF FN must be defined before it is used, and only at module level (not inside a SUB or FUNCTION). Names in the expression other than the parameters are module variables, SHARED or not; the parameters are always passed by value.

    @@ -3072,7 +3135,7 @@ EXIT DO EXIT SUB EXIT FUNCTION EXIT PROPERTY -

    EXIT PROPERTY leaves a Property Get, Let or Set procedure of a class module (see Objects and Class Modules).

    +

    EXIT PROPERTY leaves a Property Get, Let, or Set procedure of a class module (see Objects and Class Modules).

    CALL

    Explicitly calls a subroutine or function. The return value (if any) is discarded.

    CALL name
    @@ -3083,7 +3146,7 @@ CALL name(args)
    GOSUB label RETURN

    A label is a name followed by a colon at the start of a line, or a line number at the start of a line (see Line Numbers below). Labels belong to the SUB or FUNCTION they are in (module-level labels to the module level): a jump can only reach a label of the same procedure, the same label name can be used in several procedures, and a label may have the same name as a variable. A jump to a label that is not defined in the procedure, or to a SUB or variable name, is a compile error.

    -

    GOSUB records where it was called from, runs the code at the label, and RETURN goes back to the statement after the GOSUB. This works at module level and inside a SUB or FUNCTION alike. Each procedure call has its own set of pending GOSUBs: RETURN only returns from a GOSUB made in the same procedure call, and leaving the procedure (END SUB, EXIT SUB, EXIT FUNCTION or an error handled in a caller) discards the ones still pending. RETURN with no pending GOSUB raises error 3 (RETURN without GOSUB). To leave a SUB or FUNCTION, use EXIT SUB / EXIT FUNCTION.

    +

    GOSUB records where it was called from, runs the code at the label, and RETURN goes back to the statement after the GOSUB. This works at module level and inside a SUB or FUNCTION alike. Each procedure call has its own set of pending GOSUBs: RETURN only returns from a GOSUB made in the same procedure call, and leaving the procedure (END SUB, EXIT SUB, EXIT FUNCTION, or an error handled in a caller) discards the ones still pending. RETURN with no pending GOSUB raises error 3 (RETURN without GOSUB). To leave a SUB or FUNCTION, use EXIT SUB / EXIT FUNCTION.

    GoSub Initialize
     Print "Done"
     End
    @@ -3106,7 +3169,7 @@ End Sub
    ON expression GOSUB label1, label2, ...

    If the expression evaluates to 1, control goes to the first label; 2, the second; and so on. If out of range, execution falls through. The labels follow the same rules as GOTO and GOSUB (same procedure only).

    Line Numbers

    -

    As in QBasic, a line can start with a line number, a whole number from 0 to 65529. A line number is a label: GOTO, GOSUB, ON ... GOTO / GOSUB, ON ERROR GOTO, RESUME and RESTORE can name it, it belongs to its procedure like any other label, and the same number cannot start two lines of a procedure. Numbers need not be in order, lines without a number are fine, and the number is read as a value, so 010 and 10 name the same line. IF can branch to a line number directly: IF x THEN 100 ELSE 200 is IF x THEN GOTO 100 ELSE GOTO 200, and IF x GOTO 100 works too.

    +

    As in QBasic, a line can start with a line number, a whole number from 0 to 65529. A line number is a label: GOTO, GOSUB, ON ... GOTO / GOSUB, ON ERROR GOTO, RESUME, and RESTORE can name it, it belongs to its procedure like any other label, and the same number cannot start two lines of a procedure. Numbers need not be in order, lines without a number are fine, and the number is read as a value, so 010 and 10 name the same line. IF can branch to a line number directly: IF x THEN 100 ELSE 200 is IF x THEN GOTO 100 ELSE GOTO 200, and IF x GOTO 100 works too.

    10 INPUT "Guess"; g
     20 IF g = 7 THEN 50
     30 PRINT "No"
    @@ -3137,14 +3200,14 @@ PRINT USING format$; expression [; expression] ...
    Print "X="; x, "Y="; y ' comma = next 14-column zone Print #1, "Written to file"

    LPRINT

    -

    Prints on the printer instead of the screen. Everything PRINT accepts works the same way: ; and , between items, SPC, TAB and LPRINT USING. The printer keeps its own column for TAB and the 14-column zones.

    +

    Prints on the printer instead of the screen. Everything PRINT accepts works the same way: ; and , between items, SPC, TAB, and LPRINT USING. The printer keeps its own column for TAB and the 14-column zones.

    LPRINT [expression [{; | ,} expression] ...]
     LPRINT USING format$; expression [; expression] ...

    The output goes to the printer chosen in the Control Panel (a printer port, or a file). Each LPRINT is sent at once, and lines end the way printers expect. Nothing starts a new page by itself: LPRINT CHR$(12) ejects the page. If the printer has a problem (out of paper, off line), the user is asked to fix it and choose Retry, or Cancel; after Cancel the LPRINT raises error 57. With no printer to print on, LPRINT raises error 68.

    LPrint "Invoice"; Tab(40); Date$
     LPrint Using "Total: $$####.##"; total
     LPrint Chr$(12)                 ' eject the page
    -

    For a whole document with margins, a header, a footer and page numbers, and for the Print dialog, use the Printing Library (print.bas, see Include Libraries).

    +

    For a whole document with margins, a header, a footer, and page numbers, and for the Print dialog, use the Printing Library (print.bas, see Include Libraries).

    PRINT USING

    PRINT USING formats each following expression according to a format string and prints the result. The expressions after the format are separated by ; or , (both work the same here). The format string is a mix of fields and literal text: each expression consumes the next field, the literal text before it is printed as it is, and the literal text after the last field is printed at the end. If more expressions are supplied than there are fields, the format string is reused from the start.

      Format character   Meaning
    @@ -3180,13 +3243,13 @@ INPUT "prompt"; variable
     INPUT #channel, variable
    Input "Enter your name: "; name$
     Input #1, line$
    -

    The target may be a variable, an array element or a TYPE field (INPUT, INPUT #, LINE INPUT # and READ alike). INPUT and INPUT # read a number into a numeric target the way VAL does (12abc gives 12).

    +

    The target may be a variable, an array element, or a TYPE field (INPUT, INPUT #, LINE INPUT #, and READ alike). INPUT and INPUT # read a number into a numeric target the way VAL does (12abc gives 12).

    DATA / READ / RESTORE

    Inline data pool for constants.

    DATA value1, value2, "string", ...
     READ variable1, variable2, ...
     RESTORE [label]
    -

    DATA statements define a pool of values. READ reads the next value from the pool into a variable, array element or TYPE field. RESTORE resets the read pointer to the beginning; RESTORE label (or a line number) makes the next READ take the first DATA item at or after that label. An item in quotes is a string; an unquoted item is taken as raw text up to the next comma, colon, comment or end of line (so keywords, several words and text such as 12abc are read as they are written) and converted when READ into a numeric variable, which raises error 13 (Type mismatch) when the item is not a number. The most negative Integer and Long values (-32768, -2147483648) are accepted.

    +

    DATA statements define a pool of values. READ reads the next value from the pool into a variable, array element, or TYPE field. RESTORE resets the read pointer to the beginning; RESTORE label (or a line number) makes the next READ take the first DATA item at or after that label. An item in quotes is a string; an unquoted item is taken as raw text up to the next comma, colon, comment, or end of line (so keywords, several words, and text such as 12abc are read as they are written) and converted when READ into a numeric variable, which raises error 13 (Type mismatch) when the item is not a number. The most negative Integer and Long values (-32768, -2147483648) are accepted.

    Data 10, 20, 30, "Hello"
     Read a, b, c, msg$
     Print a; b; c; msg$
    @@ -3211,7 +3274,7 @@ ERROR n                 ' Raise a runtime error with error number n
  • An error raised inside a running handler passes to the calling procedure's handler (untrapped if there is none).
  • The label named by ON ERROR GOTO must be in the same procedure.
  • -

    An error that no handler traps ends the program, as in Visual Basic, wherever it happens: in module-level code or in an event handler (a Click, a Timer, a form's Load ...). DVX shows it in a BASIC Runtime Error box -- the error number, the line and the message -- and writes it to the DVX log; the program's windows close once the box is dismissed. In the IDE the Output window shows the error and the editor goes to its line.

    +

    An error that no handler traps ends the program, as in Visual Basic, wherever it happens: in module-level code or in an event handler (a Click, a Timer, a form's Load ...). DVX shows it in a BASIC Runtime Error box -- the error number, the line, and the message -- and writes it to the DVX log; the program's windows close once the box is dismissed. In the IDE the Output window shows the error and the editor goes to its line.

    ON ERROR RESUME NEXT, as in Visual Basic, needs no handler: a statement that fails is abandoned and the program goes on at the next one, with ERR (Err.Number) and Err.Description telling what went wrong. An error in a procedure that has no handler of its own leaves that procedure, and the program goes on after the statement that called it. The statement clears ERR, and so does the end of the procedure that used it; check ERR right after the statements that may fail:

    On Error Resume Next
     Kill "old.tmp"
    @@ -3227,14 +3290,14 @@ On Error GoTo 0
    Err.Source The source text the last Err.Raise gave ("" otherwise) Err.Raise number [, source [, desc]] Raises error number (1 to 65535) with that source and description; without a description the number's standard message Err.Clear Clears the error: ERR becomes 0, the description and source "" -

    ERROR$(n) (or ERROR(n)) gives the standard message of error number n, "User-defined error" for a number without one and "" for 0; without an argument, the current error's message.

    +

    ERROR$(n) (or ERROR(n)) gives the standard message of error number n, "User-defined error" for a number without one, and "" for 0; without an argument, the current error's message.

    On Error GoTo Failed
     Err.Raise 1001, "Loader", "The file has no header"
     Exit Sub
     Failed:
         Print Err.Number; Err.Description; " ("; Err.Source; ")"
         Resume Next
    -

    RESUME NEXT continues after the failing statement. When the error happened in the condition of an IF, ELSEIF, WHILE, DO or LOOP, the condition counts as false (the block is skipped, the loop is left); after an error in a FOR statement the loop is skipped; after an error in the SELECT CASE expression the whole SELECT CASE block is skipped; after an error in a CASE test that test counts as false (the next value or CASE is tried); after an error in an ON ... GOTO/GOSUB selector no branch is taken and execution continues with the next statement.

    +

    RESUME NEXT continues after the failing statement. When the error happened in the condition of an IF, ELSEIF, WHILE, DO, or LOOP, the condition counts as false (the block is skipped, the loop is left); after an error in a FOR statement the loop is skipped; after an error in the SELECT CASE expression the whole SELECT CASE block is skipped; after an error in a CASE test that test counts as false (the next value or CASE is tried); after an error in an ON ... GOTO/GOSUB selector no branch is taken and execution continues with the next statement.

    On Error GoTo ErrorHandler
     Open "missing.txt" For Input As #1
     Exit Sub
    @@ -3249,7 +3312,7 @@ ErrorHandler:
       3        RETURN without GOSUB
       4        Out of DATA
       5        Illegal function call (bad argument to a built-in function; an undefined math result such as a negative number to a fractional power)
    -  6        Overflow (value does not fit the target type; EXP, ^ or other math leaving the numeric range; a file size or position past 2 GB in LOF, FILELEN, LOC, SEEK, GET or PUT)
    +  6        Overflow (value does not fit the target type; EXP, ^, or other math leaving the numeric range; a file size or position past 2 GB in LOF, FILELEN, LOC, SEEK, GET, or PUT)
       7        Out of memory
       9        Subscript out of range / invalid variable or field index
       10       This array is fixed or temporarily locked (REDIM, or ERASE of a dynamic array, while one of its elements, or a field of one, is passed by reference to a procedure that is still running)
    @@ -3266,7 +3329,7 @@ ErrorHandler:
       57       Device I/O error (printing failed or was cancelled)
       58       File already exists or rename failed
       59       Bad record length (OPEN ... LEN must be 1 to 32767; GET/PUT data longer than LEN)
    -  63       Bad record number (SEEK, GET or PUT position below 1)
    +  63       Bad record number (SEEK, GET, or PUT position below 1)
       64       Bad file name (WIDTH of a device DVX does not have)
       67       Too many files open
       68       Device unavailable (no printer to print on)
    @@ -3275,7 +3338,7 @@ ErrorHandler:
       91       Object variable not set (property, method, Show/Hide/Unload on a value that is not a form or control, or on Nothing)
       93       Invalid pattern string (a LIKE pattern with a [ that has no ])
       94       Invalid use of Null (Null where a real number or text is needed; see Variants)
    -  424      Object required (SET, Is or TypeOf with a value that is not an object)
    +  424      Object required (SET, Is, or TypeOf with a value that is not an object)
       438      Object doesn't support this property or method (the object has no member of that name, or it is Private)
       446      Object doesn't support named arguments (a form or control method called with Name:=value)
       448      Named argument not found (a Name:= the method does not have; a left-out OPTIONAL Variant argument, Missing, used as a value)
    @@ -3305,8 +3368,8 @@ RANDOMIZE TIMER          ' Seed from system clock
    END
    -

    Sound, Joysticks and Frames

    -

    These statements work as they do in QBasic, on whatever sound hardware the computer has: a sound card, the PC speaker, or none at all, in which case the program runs the same but silently. Several programs can make sound at once. For sound files, tones of any shape and music files, see the sound.bas and music.bas include libraries; for more joystick detail, joystick.bas.

    +

    Sound, Joysticks, and Frames

    +

    These statements work as they do in QBasic, on whatever sound hardware the computer has: a sound card, the PC speaker, or none at all, in which case the program runs the same but silently. Several programs can make sound at once. For sound files, tones of any shape, and music files, see the sound.bas and music.bas include libraries; for more joystick detail, joystick.bas.

    BEEP

    Sounds a short beep (800 hertz for a quarter of a second).

    BEEP
    @@ -3332,7 +3395,7 @@ PLAY "T160 O3 L8 E D C D E E E4" MN Music normal: each note sounds for 7/8 of its length (the default). ML Music legato: notes sound for their whole length. MS Music staccato: notes sound for 3/4 of their length. - MF Music foreground: PLAY, SOUND and BEEP wait until the sound has finished (the default). + MF Music foreground: PLAY, SOUND, and BEEP wait until the sound has finished (the default). MB Music background: the program goes on while the music plays; it only waits when more than 32 notes are waiting. I n DVX's own: play the notes that follow on General MIDI instrument n (1 to 128; 1 is the piano, 41 the violin, 74 the flute) through the music synthesizer. I0 goes back to plain tones (the default). X "X" + VARPTR$(s$) plays the commands in s$ at that point (s$ may use X itself, up to 16 levels deep). @@ -3348,7 +3411,7 @@ PLAY "I74 O4 L8 GAB>C" ' on the flute

    PLAY(n) returns how many notes are still waiting to be played in the background (n is ignored). A game can use it to keep its music going:

    If PLAY(0) < 4 Then PLAY "MB " + tune$

    STICK and STRIG

    -

    STICK(n) returns where a joystick points, from 1 (all the way left or up) to 200 (all the way right or down), about 100 in the middle, or 0 when there is no such joystick. STICK(0) reads joystick A's X and remembers A's Y and joystick B's X and Y, which STICK(1), STICK(2) and STICK(3) then return; so call STICK(0) first.

    +

    STICK(n) returns where a joystick points, from 1 (all the way left or up) to 200 (all the way right or down), about 100 in the middle, or 0 when there is no such joystick. STICK(0) reads joystick A's X and remembers A's Y and joystick B's X and Y, which STICK(1), STICK(2), and STICK(3) then return; so call STICK(0) first.

    STRIG(n) returns True (-1) or False (0) for the buttons:

      n     Returns True when
       -     -----------------
    @@ -3360,7 +3423,7 @@ PLAY "I74 O4 L8 GAB>C"         ' on the flute
    5 joystick A's second button is down 6 joystick B's second button was pressed since the last STRIG(6) 7 joystick B's second button is down -

    Joystick A is the first joystick; joystick B the second one, or the second stick of a joystick with four axes. When no joystick is connected the keyboard stands in for joystick A (see the Control Panel's Game Controllers tab). STRIG ON, STRIG OFF and ON STRIG are not supported; test STRIG in a Timer or a loop instead.

    +

    Joystick A is the first joystick; joystick B the second one, or the second stick of a joystick with four axes. When no joystick is connected the keyboard stands in for joystick A (see the Control Panel's Game Controllers tab). STRIG ON, STRIG OFF, and ON STRIG are not supported; test STRIG in a Timer or a loop instead.

    x = STICK(0)
     y = STICK(1)
     If STRIG(0) Then FireShot
    @@ -3377,7 +3440,7 @@ Loop Until quitting

    Console and Graphics

    These statements work as they do in QBasic. The first one a program uses gives it a screen of its own: a window, titled with the program's name, the size of the screen mode. From then on PRINT and INPUT use that screen too (before it, PRINT writes to the output window and INPUT asks in a dialog box, as always). Keys typed in the window go to INKEY$ and INPUT. The window stays open when the program's code has finished, so its output can be read; closing it ends the program.

    -

    The names are not reserved: a program that has its own variable, SUB or constant called Color, Cls, Draw and so on keeps using it.

    +

    The names are not reserved: a program that has its own variable, SUB, or constant called Color, Cls, Draw, and so on keeps using it.

    Screen Modes

      Mode       Size and colours                     Text (WIDTH choices)     Pages
       ----       ----------------                     --------------------     -----
    @@ -3392,7 +3455,7 @@ Loop Until quitting
    SCREEN 12 640 x 480, 16 colours 80 x 30 or 80 x 60 1 SCREEN 13 320 x 200, 256 colours 40 x 25 1

    A program starts in SCREEN 0. SCREEN with a new mode (and WIDTH) clears the screen and restores the mode's colours: the 16 standard colours, and in SCREEN 13 the standard 256-colour palette. SCREEN with the mode the program is already in only changes the pages, so a program can flip pages in a loop. Other mode numbers, and graphics statements in SCREEN 0, are error 5 (Illegal function call). The colour switch argument is accepted and ignored.

    -

    Text uses 16-pixel-high characters in SCREEN 0 (25 rows), 11 and 12 (30 rows), 14-pixel ones in SCREEN 9 and 10 (25 rows) and 8-pixel ones everywhere else. The window shows each mode in the shape it has on a monitor: the 640 x 200 modes with every line doubled, 40-column text with every column doubled; SCREEN 9 and 10 show at 640 x 350 as they are. On the whole screen the 320 x 200 modes use the computer's 320 x 200 mode and the others the nearest mode, centred.

    +

    Text uses 16-pixel-high characters in SCREEN 0 (25 rows), 11 and 12 (30 rows), 14-pixel ones in SCREEN 9 and 10 (25 rows), and 8-pixel ones everywhere else. The window shows each mode in the shape it has on a monitor: the 640 x 200 modes with every line doubled, 40-column text with every column doubled; SCREEN 9 and 10 show at 640 x 350 as they are. On the whole screen the 320 x 200 modes use the computer's 320 x 200 mode and the others the nearest mode, centred.

    SCREEN mode [, [colorswitch] [, [activepage] [, visualpage]]]
     WIDTH [columns] [, rows]

    Screen Pages and PCOPY

    @@ -3421,7 +3484,7 @@ code = SCREEN(row, column [, colorflag])
  • CLS clears the screen and puts the cursor in the top-left corner (the top of the text viewport). In a graphics mode with a VIEW set, CLS (or CLS 1) clears only the view; with a VIEW PRINT set, CLS (or CLS 2) clears only the text viewport; CLS 0 clears everything.
  • LOCATE moves the cursor (rows and columns count from 1). cursor 1 shows the text cursor in SCREEN 0, 0 hides it (the default). QBasic's cursor size arguments are accepted and ignored.
  • COLOR in SCREEN 0 takes a foreground 0 to 31 and a background 0 to 15. Foregrounds 16 to 31 blink (colour minus 16); as on a VGA, backgrounds 8 to 15 show as 0 to 7 and make the text blink too.
  • -
  • COLOR in SCREEN 1 takes a background colour (0 to 15) and a palette: an even number gives green, red and brown for colours 1 to 3, an odd one (the default) cyan, magenta and white. SCREEN 2 has no COLOR statement. In SCREEN 7 to 10 COLOR takes a foreground and a background colour (the colour of attribute 0: 0 to 15 in SCREEN 7 and 8, an EGA colour 0 to 63 in SCREEN 9, a shade 0 to 8 in SCREEN 10). In SCREEN 11 to 13 it takes only a foreground. The foreground is also the default colour of the drawing statements.
  • +
  • COLOR in SCREEN 1 takes a background colour (0 to 15) and a palette: an even number gives green, red, and brown for colours 1 to 3, an odd one (the default) cyan, magenta, and white. SCREEN 2 has no COLOR statement. In SCREEN 7 to 10 COLOR takes a foreground and a background colour (the colour of attribute 0: 0 to 15 in SCREEN 7 and 8, an EGA colour 0 to 63 in SCREEN 9, a shade 0 to 8 in SCREEN 10). In SCREEN 11 to 13 it takes only a foreground. The foreground is also the default colour of the drawing statements.
  • CSRLIN is the cursor's row and POS(0) its column. Before the program has a screen, POS(0) is the output window's column.
  • SCREEN(row, column) is the character code shown there; with a colorflag of 1, its colour (in SCREEN 0, foreground + 16 * background).
  • PRINT scrolls the screen (or the text viewport) up at the bottom. CHR$(13) and CHR$(10) start a new line and CHR$(9) moves to the next multiple of 8 columns.
  • @@ -3430,7 +3493,7 @@ code = SCREEN(row, column [, colorflag])

    Makes rows top to bottom the text viewport: PRINT scrolls only those rows, so the rows around them stay as they are (a title, a status line). The cursor goes to the viewport's first row. VIEW PRINT alone makes the whole screen the viewport again. LOCATE can still reach every row.

    VIEW PRINT [top TO bottom]

    INKEY$

    -

    Returns the next key typed in the program's window without waiting: "" when there is none, one character for a character key, and CHR$(0) + CHR$(scan code) for other keys (arrows, function keys). Keys held with Alt, and function keys held with Shift, Ctrl or Alt, give the codes QBasic gives: Alt+A is CHR$(0) + CHR$(30), Alt+1 CHR$(0) + CHR$(120), Shift+F1 CHR$(0) + CHR$(84), Ctrl+F1 CHR$(0) + CHR$(94) and Alt+F1 CHR$(0) + CHR$(104). Keys wait for INKEY$ in a queue of 64.

    +

    Returns the next key typed in the program's window without waiting: "" when there is none, one character for a character key, and CHR$(0) + CHR$(scan code) for other keys (arrows, function keys). Keys held with Alt, and function keys held with Shift, Ctrl, or Alt, give the codes QBasic gives: Alt+A is CHR$(0) + CHR$(30), Alt+1 CHR$(0) + CHR$(120), Shift+F1 CHR$(0) + CHR$(84), Ctrl+F1 CHR$(0) + CHR$(94), and Alt+F1 CHR$(0) + CHR$(104). Keys wait for INKEY$ in a queue of 64.

    Do
         k$ = INKEY$
     Loop While k$ = ""
    @@ -3452,17 +3515,17 @@ Do
         If Pen(0) Then PSet (Pen(1), Pen(2)), 14
     Loop While InKey$ = ""

    INPUT and LINE INPUT on the Screen

    -

    On the program's screen INPUT shows its prompt at the cursor and reads what is typed there: Backspace takes back a character, Esc clears the line and Enter ends it. LINE INPUT reads a whole line, commas included, without adding "? " to the prompt. Without the program's screen LINE INPUT asks in a dialog box, like INPUT.

    +

    On the program's screen INPUT shows its prompt at the cursor and reads what is typed there: Backspace takes back a character, Esc clears the line, and Enter ends it. LINE INPUT reads a whole line, commas included, without adding "? " to the prompt. Without the program's screen LINE INPUT asks in a dialog box, like INPUT.

    LINE INPUT ["prompt";] variable$

    Graphics Coordinates

    -

    Points are (x, y) in pixels from the top-left corner. STEP(x, y) is relative to the last point drawn (the centre of the screen at first). VIEW makes a smaller part of the screen the drawing area (coordinates start at its corner, and drawing stays inside it); WINDOW gives it coordinates of your own. Colour numbers are attributes: 0 to 3 in SCREEN 1 and 10, 0 and 1 in SCREEN 2 and 11, 0 to 15 in SCREEN 7, 8, 9 and 12, 0 to 255 in SCREEN 13; anything else is error 5.

    +

    Points are (x, y) in pixels from the top-left corner. STEP(x, y) is relative to the last point drawn (the centre of the screen at first). VIEW makes a smaller part of the screen the drawing area (coordinates start at its corner, and drawing stays inside it); WINDOW gives it coordinates of your own. Colour numbers are attributes: 0 to 3 in SCREEN 1 and 10, 0 and 1 in SCREEN 2 and 11, 0 to 15 in SCREEN 7, 8, 9, and 12, 0 to 255 in SCREEN 13; anything else is error 5.

    VIEW [[SCREEN] (x1, y1)-(x2, y2) [, [fill] [, border]]]
     WINDOW [[SCREEN] (x1, y1)-(x2, y2)]
     value = PMAP(coordinate, n)

    PSET, PRESET, POINT

    PSET [STEP](x, y) [, color]
    @@ -3472,13 +3535,13 @@ v = POINT(n)

    PSET draws a point in the given colour or the COLOR foreground, PRESET in the given colour or colour 0. POINT(x, y) is the colour at a point, or -1 outside the view. POINT(0) and POINT(1) are the last point in pixels, POINT(2) and POINT(3) in WINDOW coordinates.

    LINE

    LINE [[STEP](x1, y1)]-[STEP](x2, y2) [, [color] [, [B | BF] [, style]]]
    -

    Draws a line, a box (B) or a filled box (BF). Without a start point the line starts at the last point. style is a 16-bit pattern: each set bit (from the top) draws a pixel, each clear one skips it.

    +

    Draws a line, a box (B), or a filled box (BF). Without a start point the line starts at the last point. style is a 16-bit pattern: each set bit (from the top) draws a pixel, each clear one skips it.

    LINE (10, 10)-(100, 50), 4, BF
     LINE -STEP(20, 0), 15
     LINE (0, 100)-(319, 100), 7, , &HF0F0

    CIRCLE

    CIRCLE [STEP](x, y), radius [, [color] [, [start] [, [end] [, aspect]]]]
    -

    Draws a circle, an ellipse or an arc. start and end are angles in radians (0 to 2 * pi, counterclockwise from the right); a negative angle also draws the radius to that end, so CIRCLE can draw pie slices. aspect is the height divided by the width; the default makes circles round on a 4:3 monitor (0.83 in SCREEN 13, 1 in SCREEN 12), so in SCREEN 13 a circle in the window looks slightly flat and a round one needs aspect 1.

    +

    Draws a circle, an ellipse, or an arc. start and end are angles in radians (0 to 2 * pi, counterclockwise from the right); a negative angle also draws the radius to that end, so CIRCLE can draw pie slices. aspect is the height divided by the width; the default makes circles round on a 4:3 monitor (0.83 in SCREEN 13, 1 in SCREEN 12), so in SCREEN 13 a circle in the window looks slightly flat and a round one needs aspect 1.

    CIRCLE (160, 100), 50, 14
     CIRCLE (160, 100), 50, 4, -0.0001, -1.5708    ' a quarter pie slice
     CIRCLE (160, 100), 80, 2, , , 0.5              ' an ellipse
    @@ -3486,7 +3549,7 @@ CIRCLE (160, 100), 80, 2, , , 0.5 ' an ellipse
    PAINT [STEP](x, y) [, [color] [, border]]
     PAINT [STEP](x, y), tile$ [, [border] [, background$]]

    Fills the area around the point with color, up to pixels of the border colour (by default the fill colour itself).

    -

    With a string instead of a colour, PAINT fills the area with a pattern, as in QBasic. Each row of the pattern is one byte for each colour plane of the mode, and each bit is a pixel (the first bit is the left pixel): in SCREEN 2 and 11 one byte is eight pixels, colour 0 or 1; in SCREEN 7, 8, 9 and 12 four bytes (the blue, green, red and bright planes) make eight pixels of 16 colours; in SCREEN 10 two bytes. In SCREEN 1 each byte is four pixels of two bits, and in SCREEN 13 each byte is one pixel. A pattern has up to 64 rows and repeats over the screen. The fill stops at the border colour (by default the COLOR foreground) and covers the whole area once, even where it already has the pattern's colours. background$ is accepted as in QBasic: a pattern with more than two rows in a row equal to it is error 5.

    +

    With a string instead of a colour, PAINT fills the area with a pattern, as in QBasic. Each row of the pattern is one byte for each colour plane of the mode, and each bit is a pixel (the first bit is the left pixel): in SCREEN 2 and 11 one byte is eight pixels, colour 0 or 1; in SCREEN 7, 8, 9, and 12 four bytes (the blue, green, red, and bright planes) make eight pixels of 16 colours; in SCREEN 10 two bytes. In SCREEN 1 each byte is four pixels of two bits, and in SCREEN 13 each byte is one pixel. A pattern has up to 64 rows and repeats over the screen. The fill stops at the border colour (by default the COLOR foreground) and covers the whole area once, even where it already has the pattern's colours. background$ is accepted as in QBasic: a pattern with more than two rows in a row equal to it is error 5.

    SCREEN 12
     LINE (0, 0)-(100, 100), 15, B
     PAINT (50, 50), CHR$(&HAA) + CHR$(&H55) + CHR$(0) + CHR$(0), 15      ' blue and green stripes
    @@ -3510,7 +3573,7 @@ PAINT (50, 50), CHR$(&HAA) + CHR$(&H55) + CHR$(0) + CHR$(0), 15 ' b A n Turn the directions by n * 90 degrees (0 to 3). TA n Turn the directions by n degrees (-360 to 360). P c,b PAINT at the current point with colour c up to border colour b. -

    The pen starts at the last point and stays where it ends. The colour, scale and turn stay from one DRAW to the next. A mistake in the string is error 5.

    +

    The pen starts at the last point and stays where it ends. The colour, scale, and turn stay from one DRAW to the next. A mistake in the string is error 5.

    As in QBasic, X runs another DRAW string and "=" takes a number, both given with VARPTR$: DRAW "X" + VARPTR$(shape$) and DRAW "C=" + VARPTR$(c) + "R=" + VARPTR$(n). DVX BASIC has no memory addresses, so VARPTR$ packs the value itself into its result (taken when VARPTR$ runs), and it takes any expression. Building the string with + and STR$ works too, such as DRAW "R" + STR$(n).

    DRAW "BM100,100 C14 R50 D50 L50 U50"      ' a square
     DRAW "BM160,100 TA45 C2 R40"              ' a line at 45 degrees
    @@ -3521,7 +3584,7 @@ NEXT i

    PALETTE

    PALETTE [attribute, color]
     PALETTE USING array[(first)]
    -

    Changes the colour an attribute shows. In SCREEN 11, 12 and 13, color is blue * 65536 + green * 256 + red with each part 0 to 63; in SCREEN 0 and 9 it is an EGA colour number 0 to 63; in SCREEN 1, 2, 7 and 8 one of the 16 standard colours; in SCREEN 10 a shade 0 to 8 (0 off, 4 grey, 8 bright, and 1 to 3, 5 to 7 blinking between two of them). PALETTE alone restores the mode's colours. PALETTE USING sets every attribute of the mode (256 in SCREEN 13) from a one-dimensional LONG array starting at element first (or its first element); -1 leaves that attribute as it is. On the whole screen a palette change shows at once, like on a VGA card; in the window, whatever is already drawn in that attribute changes colour too.

    +

    Changes the colour an attribute shows. In SCREEN 11, 12, and 13, color is blue * 65536 + green * 256 + red with each part 0 to 63; in SCREEN 0 and 9 it is an EGA colour number 0 to 63; in SCREEN 1, 2, 7, and 8 one of the 16 standard colours; in SCREEN 10 a shade 0 to 8 (0 off, 4 grey, 8 bright, and 1 to 3, 5 to 7 blinking between two of them). PALETTE alone restores the mode's colours. PALETTE USING sets every attribute of the mode (256 in SCREEN 13) from a one-dimensional LONG array starting at element first (or its first element); -1 leaves that attribute as it is. On the whole screen a palette change shows at once, like on a VGA card; in the window, whatever is already drawn in that attribute changes colour too.

    SCREEN 13
     PALETTE 1, 63 * 65536        ' attribute 1 becomes bright blue
     FOR i = 0 TO 63
    @@ -3573,11 +3636,11 @@ CLOSE
     RESET

    CLOSE with a list closes each channel in it (the # is optional); CLOSE alone, and RESET, close every open file. Closing a channel that is not open does nothing; a number outside the channels DVX has raises error 52.

    PRINT #

    -

    Writes text to a file, formatted exactly as PRINT shows it: numbers are followed by a space, a comma moves to the next 14-column zone, a semicolon keeps the line going, and TAB(n), SPC(n) and PRINT #channel, USING work as they do for PRINT. See PRINT.

    +

    Writes text to a file, formatted exactly as PRINT shows it: numbers are followed by a space, a comma moves to the next 14-column zone, a semicolon keeps the line going, and TAB(n), SPC(n), and PRINT #channel, USING work as they do for PRINT. See PRINT.

    PRINT #channel, expression [; | , expression ...]
     PRINT #channel, USING format$; expression [; expression ...]

    WIDTH #

    -

    Sets the line width of an open file, the screen or the printer: output longer than the width goes on in a new line, as in QBasic. 255 means no limit (the default).

    +

    Sets the line width of an open file, the screen, or the printer: output longer than the width goes on in a new line, as in QBasic. 255 means no limit (the default).

    WIDTH #channel, width
     WIDTH "SCRN:", width
     WIDTH "LPT1:", width
    @@ -3601,7 +3664,7 @@ Close #1

    Read and write records in RANDOM or BINARY mode files. When recordNum is omitted the transfer starts at the current file position.

    GET #channel, [recordNum], variable
     PUT #channel, [recordNum], variable
    -

    In RANDOM mode recordNum is a 1-based record number and each record is LEN bytes (see OPEN). Numbers are stored in their binary form (Byte 1 byte, Integer and Boolean 2, Long 4, Single 4, Double, Currency and Date 8); a String is stored as a 2-byte length followed by its characters. PUT always writes a whole record, padding the unused part with zero bytes, and GET always advances to the next record; data longer than LEN raises error 59 before anything is written.

    +

    In RANDOM mode recordNum is a 1-based record number and each record is LEN bytes (see OPEN). Numbers are stored in their binary form (Byte 1 byte, Integer and Boolean 2, Long 4, Single 4, Double, Currency, and Date 8); a String is stored as a 2-byte length followed by its characters. PUT always writes a whole record, padding the unused part with zero bytes, and GET always advances to the next record; data longer than LEN raises error 59 before anything is written.

    In BINARY mode recordNum is a 1-based byte position. Numbers are stored as in RANDOM mode. PUT writes the characters of a String with no length prefix, and GET reads as many bytes as the String variable currently holds, so set its length first (for example with SPACE$ or by declaring it as STRING * n).

    The variable may be a TYPE variable. Its fields are packed in declaration order with the sizes above; a STRING * n field takes exactly n bytes (padded with spaces), a variable-length String field is stored with its 2-byte length prefix in both modes, and nested TYPE fields are packed inline. GET fills the fields of the existing variable in place.

    The variable may also be an array element, arr(i), of any type including a TYPE: PUT writes that element and GET reads into it. The index expression is evaluated once.

    @@ -3630,7 +3693,7 @@ pos = SEEK(channel) ' Function: get current position

    File System Statements

    DVX BASIC provides statements for working with the file system -- creating, deleting, renaming, and copying files and directories, and reading or setting file attributes.

    -

    A path may be written with / or \ between its parts, mixed too: OPEN "DATA/SCORES.TXT" and OPEN "DATA\SCORES.TXT" open the same file, and so do the FileOps, Shell and other library functions that take a path (SHELL commands are the exception: see SHELL). Paths DVX gives back -- CURDIR$, App.Path, App.Data, the file dialogs, FileOpsPathJoin$ and the other FileOps results -- use \, the separator DOS shows; FileOpsPathNative$ gives a path of your own that form before you show it.

    +

    A path may be written with / or \ between its parts, mixed too: OPEN "DATA/SCORES.TXT" and OPEN "DATA\SCORES.TXT" open the same file, and so do the FileOps, Shell, and other library functions that take a path (SHELL commands are the exception: see SHELL). Paths DVX gives back -- CURDIR$, App.Path, App.Data, the file dialogs, FileOpsPathJoin$, and the other FileOps results -- use \, the separator DOS shows; FileOpsPathNative$ gives a path of your own that form before you show it.

    KILL

    Deletes a file.

    KILL filename$
    @@ -3673,7 +3736,7 @@ End If SetAttr "secret.dat", vbHidden + vbReadOnly

    CURDIR$ / DIR$

    -

    CURDIR$ returns the current directory. DIR$ returns the first file matching a pattern; subsequent calls with no arguments return additional matches (or "" when done). DIR$ returns only files: never ".", ".." or subdirectories. DIR$("C:\*.*") lists the root of C: (DIR$("C:*.*") the drive's current folder).

    +

    CURDIR$ returns the current directory. DIR$ returns the first file matching a pattern; subsequent calls with no arguments return additional matches (or "" when done). DIR$ returns only files: never ".", "..", or subdirectories. DIR$("C:\*.*") lists the root of C: (DIR$("C:*.*") the drive's current folder).

    cwd$ = CURDIR$
     first$ = DIR$("*.txt")
     WHILE first$ <> ""
    @@ -3716,17 +3779,17 @@ WEND
    TRIM$(s$) String Removes leading and trailing spaces from s$ UCASE$(s$) String Converts s$ to uppercase VAL(s$) Double Converts string s$ to a numeric value; stops at first non-numeric character -

    A negative count or position passed to LEFT$, RIGHT$, MID$, SPACE$ or STRING$, or a count above 65535 passed to SPACE$ or STRING$, raises error 5 (Illegal function call). INSTR follows OPTION COMPARE TEXT (a case-insensitive search), and an INSTR start position below 1 raises error 5.

    -

    REPLACE$, JOIN$ and STRREVERSE$ may also be written without the $. Items in square brackets may be left out.

    +

    A negative count or position passed to LEFT$, RIGHT$, MID$, SPACE$, or STRING$, or a count above 65535 passed to SPACE$ or STRING$, raises error 5 (Illegal function call). INSTR follows OPTION COMPARE TEXT (a case-insensitive search), and an INSTR start position below 1 raises error 5.

    +

    REPLACE$, JOIN$, and STRREVERSE$ may also be written without the $. Items in square brackets may be left out.

    Comparing Text

    -

    The compare argument of FILTER, INSTRREV, REPLACE$, SPLIT and STRCOMP chooses how text is matched: vbBinaryCompare (0) matches exactly, vbTextCompare (1) ignores upper and lower case, and vbUseCompareOption (-1, the default) follows OPTION COMPARE. Any other value raises error 5.

    +

    The compare argument of FILTER, INSTRREV, REPLACE$, SPLIT, and STRCOMP chooses how text is matched: vbBinaryCompare (0) matches exactly, vbTextCompare (1) ignores upper and lower case, and vbUseCompareOption (-1, the default) follows OPTION COMPARE. Any other value raises error 5.

    Print StrComp("apple", "Apple")                  ' 1 ("a" sorts after "A")
     Print StrComp("apple", "Apple", vbTextCompare)   ' 0
     Print Replace$("Red red RED", "red", "blue", 1, -1, vbTextCompare)   ' blue blue blue
     Print InStrRev("C:\DVX\APPS\EDIT.APP", "\")     ' 12

    REPLACE$ Details

    REPLACE$ returns the text from position start on, so REPLACE$("abcabc", "b", "x", 3) is "caxc" -- the characters before start are left out, as in Visual Basic. A count of -1 replaces every occurrence. An empty find$ returns the text unchanged. A start below 1 or a count below -1 raises error 5.

    -

    SPLIT, JOIN$ and FILTER

    +

    SPLIT, JOIN$, and FILTER

    SPLIT returns a string array numbered from 0. Assign it to a dynamic array (one declared with empty parentheses) and use UBOUND to find the last piece. The last piece keeps the rest of the text when limit stops the split. Splitting an empty string, or a limit of 0, gives an array with no elements: its UBOUND is -1. An empty delim$ gives the whole string as the only piece.

    Dim parts() As String
     Dim i As Integer
    @@ -3748,11 +3811,11 @@ Print Format$(42, "00000")            ' "00042"
     Print Format$(1234.5, "#,##0.00")     ' "1,234.50"
     Print Format$(5, "#,##0.00")          ' "    5.00"
     Print Format$(0.5, "PERCENT")         ' "50%"
    -

    The whole format string is one numeric field (for a number). The accepted format characters are # (digit or pad space), 0 (digit that is always printed; zero pads up to the first 0), . (decimal point), , (thousands separator), + and - (sign placement), $$ (floating dollar sign), ** (asterisk fill), ^^^^ (scientific notation) and the literal word PERCENT (multiplies by 100 and appends %). Unlike PRINT USING, a value wider than the field is printed in full without a % marker. See PRINT USING for details on each character.

    +

    The whole format string is one numeric field (for a number). The accepted format characters are # (digit or pad space), 0 (digit that is always printed; zero pads up to the first 0), . (decimal point), , (thousands separator), + and - (sign placement), $$ (floating dollar sign), ** (asterisk fill), ^^^^ (scientific notation), and the literal word PERCENT (multiplies by 100 and appends %). Unlike PRINT USING, a value wider than the field is printed in full without a % marker. See PRINT USING for details on each character.

    With no format string (or an empty one), FORMAT$ gives the value's plain text, as CSTR does: a number without the leading space STR$ puts before it (Format$(42) is "42"), a date in the usual m/d/yyyy h:mm:ss AM form, text as it is, True / False, and "" for Empty.

    FORMAT (without the $) is the same function.

    FORMAT$ with Dates

    -

    When the format string holds a date or time code, or is one of the named date formats, FORMAT$ formats its value as a date. The value may be a Date, a number (taken as a date's number of days) or a string that holds a date (anything else raises error 13). A Date with an empty format string gives the usual m/d/yyyy h:mm:ss AM form. The codes may be written in upper or lower case.

    +

    When the format string holds a date or time code, or is one of the named date formats, FORMAT$ formats its value as a date. The value may be a Date, a number (taken as a date's number of days), or a string that holds a date (anything else raises error 13). A Date with an empty format string gives the usual m/d/yyyy h:mm:ss AM form. The codes may be written in upper or lower case.

      Code          Result
       ----          ------
       d / dd        Day of the month (dd: two digits)
    @@ -3818,10 +3881,10 @@ Print s$                   ' Hello, DVX--!
    TAN(n) Double Tangent of n (n in radians) TIMER Double Seconds since midnight

    RND follows QBasic: RND or RND(n) with n > 0 returns the next number in the sequence, RND(0) repeats the last number, and RND(n) with n < 0 reseeds the generator from n (the same n always gives the same number). The result is a Double, 0 <= RND < 1.

    -

    INT, FIX and SGN return an Integer, widening to Long or Double when the value does not fit. EXP, ^ and the other functions raise error 6 (Overflow) when the result leaves the numeric range, and error 5 when it is undefined (for example a negative number to a fractional power).

    +

    INT, FIX, and SGN return an Integer, widening to Long or Double when the value does not fit. EXP, ^, and the other functions raise error 6 (Overflow) when the result leaves the numeric range, and error 5 when it is undefined (for example a negative number to a fractional power).

    Note: Use RANDOMIZE or RANDOMIZE TIMER to seed the generator so you don't get the same sequence each run.

    Color Functions

    -

    The color functions work with 24-bit colors packed into a Long the way Visual Basic packs them, &HBBGGRR: red is the low byte, green the middle byte and blue the high byte. RGB(255, 0, 0) and &HFF are red, RGB(0, 0, 255) and &HFF0000 are blue. ForeColor, BackColor, FillColor, the drawing methods of forms and picture boxes (PSet, Line, Circle, Point), the Canvas control and the color dialog all use this form (the console graphics statements of SCREEN modes use QBasic color numbers instead), as do the color constants vbBlack, vbRed, vbGreen, vbYellow, vbBlue, vbMagenta, vbCyan and vbWhite (see Predefined Constants).

    +

    The color functions work with 24-bit colors packed into a Long the way Visual Basic packs them, &HBBGGRR: red is the low byte, green the middle byte, and blue the high byte. RGB(255, 0, 0) and &HFF are red, RGB(0, 0, 255) and &HFF0000 are blue. ForeColor, BackColor, FillColor, the drawing methods of forms and picture boxes (PSet, Line, Circle, Point), the Canvas control, and the color dialog all use this form (the console graphics statements of SCREEN modes use QBasic color numbers instead), as do the color constants vbBlack, vbRed, vbGreen, vbYellow, vbBlue, vbMagenta, vbCyan, and vbWhite (see Predefined Constants).

      Function           Returns   Description
       --------           -------   -----------
       RGB(r, g, b)       Long      Combine red, green, and blue components (0-255) into a color: r + g * 256 + b * 65536
    @@ -3849,12 +3912,12 @@ Me.ForeColor = QBColor(14)     ' yellow
    CSNG(n) Single Converts n to Single CSTR(n) String Converts n to its String representation (a Date gives m/d/yyyy h:mm:ss AM) CVAR(v) Variant Returns v unchanged as a Variant -

    Passing Null to any of these except CVAR raises error 94 (Invalid use of Null). Text passed to one that gives a number must hold a number (CINT("12") is 12); other text raises error 13 (Type mismatch). VARTYPE, TYPENAME and the IS functions (ISNULL, ISEMPTY, ISNUMERIC, ...) are described under Variants.

    +

    Passing Null to any of these except CVAR raises error 94 (Invalid use of Null). Text passed to one that gives a number must hold a number (CINT("12") is 12); other text raises error 13 (Type mismatch). VARTYPE, TYPENAME, and the IS functions (ISNULL, ISEMPTY, ISNUMERIC, ...) are described under Variants.

    See also: Variants

    Date and Time Functions

    -

    These functions work with Date values (see Data Types). Wherever a date is expected, a Date, a number of days since 12/30/1899 or a string holding a date may be given; text that is not a date raises error 13 (Type mismatch).

    +

    These functions work with Date values (see Data Types). Wherever a date is expected, a Date, a number of days since 12/30/1899, or a string holding a date may be given; text that is not a date raises error 13 (Type mismatch).

      Function                                                 Returns   Description
       --------                                                 -------   -----------
       NOW                                                      Date      The current date and time
    @@ -3877,9 +3940,9 @@ Me.ForeColor = QBColor(14)     ' yellow
    ISDATE(v) Boolean True when v is a Date or a string holding a valid date MONTHNAME(month [, abbreviate]) String January ... December (Jan ... Dec when abbreviate is True) WEEKDAYNAME(n [, abbreviate [, firstDay]]) String Name of day n of the week counted from firstDay (default vbSunday) -

    NOW, DATE and TIME need no parentheses. DATE$ and TIME$ still return the date and time as text.

    +

    NOW, DATE, and TIME need no parentheses. DATE$ and TIME$ still return the date and time as text.

    Intervals

    -

    DATEADD, DATEDIFF and DATEPART take the interval as a string, in upper or lower case:

    +

    DATEADD, DATEDIFF, and DATEPART take the interval as a string, in upper or lower case:

      Interval   Meaning
       --------   -------
       "yyyy"     Year
    @@ -3894,12 +3957,12 @@ Me.ForeColor = QBColor(14)     ' yellow
    "s" Second

    Any other interval raises error 5 (Illegal function call).

    DATESERIAL and TIMESERIAL

    -

    A year from 0 to 29 means 2000 to 2029 and one from 30 to 99 means 1930 to 1999. Months, days, hours, minutes and seconds outside their usual range carry into the next or previous unit, so DATESERIAL(2000, 13, 1) is 1/1/2001, DATESERIAL(2000, 3, 0) is the last day of February 2000 and TIMESERIAL(0, 90, 0) is 1:30 AM. Fractional arguments are rounded. A result before 1/1/100 or after 12/31/9999 raises error 5.

    +

    A year from 0 to 29 means 2000 to 2029 and one from 30 to 99 means 1930 to 1999. Months, days, hours, minutes, and seconds outside their usual range carry into the next or previous unit, so DATESERIAL(2000, 13, 1) is 1/1/2001, DATESERIAL(2000, 3, 0) is the last day of February 2000, and TIMESERIAL(0, 90, 0) is 1:30 AM. Fractional arguments are rounded. A result before 1/1/100 or after 12/31/9999 raises error 5.

    Dim lastDay As Date
     lastDay = DateSerial(Year(Date), Month(Date) + 1, 0)    ' last day of this month
     Print Day(lastDay)

    DATEADD

    -

    Adding months, quarters or years keeps the day of the month where it can; when the new month is shorter the day becomes its last day, so DATEADD("m", 1, #1/31/2000#) is 2/29/2000. The time of day is kept. "y", "d" and "w" all add days and "ww" adds weeks. A fractional n drops its fraction. A result outside the Date range raises error 5.

    +

    Adding months, quarters, or years keeps the day of the month where it can; when the new month is shorter the day becomes its last day, so DATEADD("m", 1, #1/31/2000#) is 2/29/2000. The time of day is kept. "y", "d", and "w" all add days and "ww" adds weeks. A fractional n drops its fraction. A result outside the Date range raises error 5.

    Print DateAdd("m", 1, #1/31/2001#)      ' 2/28/2001
     Print DateAdd("h", -2, #1/1/2000#)      ' 12/31/1999 10:00:00 PM

    DATEDIFF

    @@ -3918,7 +3981,7 @@ Print DateDiff("w", #1/1/2000#, #1/2/2000#) ' 0

    Days before week 1 belong to the last week of the previous year. A firstDay or firstWeek outside these values raises error 5.

    Print DatePart("q", #5/15/2000#)                               ' 2
     Print DatePart("ww", #1/1/2000#, vbSunday, vbFirstFourDays)    ' 52
    -

    ISDATE, MONTHNAME and WEEKDAYNAME

    +

    ISDATE, MONTHNAME, and WEEKDAYNAME

    ISDATE is True for a Date and for a string that holds a valid date or time; numbers are not dates to ISDATE. MONTHNAME with a month outside 1 to 12, and WEEKDAYNAME with n outside 1 to 7, raise error 5.

    If IsDate(Text1.Text) Then due = CDate(Text1.Text)
     Print MonthName(Month(Date)); " "; WeekdayName(Weekday(Date), True)
    @@ -3943,7 +4006,7 @@ Print MonthName(Month(Date)); " "; WeekdayName(Weekday(Date), True)

    The dim argument of LBOUND and UBOUND must be a constant from 1 to 8.

    -

    File sizes and positions are Longs, so they stop at 2 GB (2,147,483,647 bytes). A disk can hold bigger files (up to 4 GB on FAT32); LOF, FILELEN, LOC and SEEK raise error 6 (Overflow) for a size or position past 2 GB rather than give a wrong number, and GET and PUT do the same for a record that would end past it. EOF stays right while the position is below 2 GB. FileOpsListSize and FileOpsTreeBytes (fileops.bas) give any file's size as a Double.

    +

    File sizes and positions are Longs, so they stop at 2 GB (2,147,483,647 bytes). A disk can hold bigger files (up to 4 GB on FAT32); LOF, FILELEN, LOC, and SEEK raise error 6 (Overflow) for a size or position past 2 GB rather than give a wrong number, and GET and PUT do the same for a record that would end past it. EOF stays right while the position is below 2 GB. FileOpsListSize and FileOpsTreeBytes (fileops.bas) give any file's size as a Double.

    Miscellaneous Functions

    @@ -3959,15 +4022,15 @@ Print MonthName(Month(Date)); " "; WeekdayName(Weekday(Date), True) + LOADPICTURE([file$ [, size [, colordepth [, x, y]]]]) Object A Picture object for a picture property (DragIcon, Picture), with Width, Height, and Type; no file (or "") gives an empty picture

    DATE$ and TIME$ return text; the Date and Time Functions return Date values.

    IIF and CHOOSE

    IIF and CHOOSE evaluate every argument before picking one, as in Visual Basic, so a function called in an argument runs even when its value is not chosen. A fractional n in CHOOSE drops its fraction (2.9 picks the second value). An n below 1 or past the end of the list gives Null, as in Visual Basic (test it with ISNULL; see Variants).

    Print IIf(score >= 50, "Pass", "Fail")
     Print Choose(Weekday(Date), "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")

    LOADPICTURE

    -

    LOADPICTURE(file$) reads a picture file (BMP, PNG, JPEG or GIF) -- or the icon of a DVX program, widget or library file (.app, .wgt, .lib) -- and returns a Picture object (TypeName "Picture") that can be given to a picture property: a control's DragIcon, the Picture of an Image or ImageButton. Use SET, or a plain assignment, as with a file name; file names still work too. LOADPICTURE() or LOADPICTURE("") gives an empty picture, which clears the property. A missing file raises error 53 (File not found), a file that is not a picture error 481 (Invalid picture), so the check happens where the picture is loaded. Loading the same file again gives the same Picture object. Reading a picture property back gives a Picture object too (TypeName "Picture"), as in Visual Basic; used as a value (printed, joined to text, compared) it stands for its file name, so code that treats the property as a file name keeps working. The name of a program, widget or library file works wherever a picture file's name does (a Picture property, DragIcon, PaintPicture, a Toolbar's AddButton) and shows its icon; such a file without an icon is not a picture (error 481).

    -

    The optional size and colordepth arguments are Visual Basic's: size is vbLPSmall (0), vbLPLarge (1), vbLPSmallShell (2), vbLPLargeShell (3) or vbLPCustom (4, with x and y giving the size), colordepth vbLPDefault (0), vbLPMonochrome (1), vbLPVGAColor (2) or vbLPColor (3). They choose among the images of an icon file; every DVX picture is a single bitmap, so, as for a bitmap in Visual Basic, they are checked and otherwise make no difference. A value outside these, or a negative x or y, raises error 5.

    +

    LOADPICTURE(file$) reads a picture file (BMP, PNG, JPEG, or GIF) -- or the icon of a DVX program, widget, or library file (.app, .wgt, .lib) -- and returns a Picture object (TypeName "Picture") that can be given to a picture property: a control's DragIcon, the Picture of an Image or ImageButton. Use SET, or a plain assignment, as with a file name; file names still work too. LOADPICTURE() or LOADPICTURE("") gives an empty picture, which clears the property. A missing file raises error 53 (File not found), a file that is not a picture error 481 (Invalid picture), so the check happens where the picture is loaded. Loading the same file again gives the same Picture object. Reading a picture property back gives a Picture object too (TypeName "Picture"), as in Visual Basic; used as a value (printed, joined to text, compared) it stands for its file name, so code that treats the property as a file name keeps working. The name of a program, widget, or library file works wherever a picture file's name does (a Picture property, DragIcon, PaintPicture, a Toolbar's AddButton) and shows its icon; such a file without an icon is not a picture (error 481).

    +

    The optional size and colordepth arguments are Visual Basic's: size is vbLPSmall (0), vbLPLarge (1), vbLPSmallShell (2), vbLPLargeShell (3), or vbLPCustom (4, with x and y giving the size), colordepth vbLPDefault (0), vbLPMonochrome (1), vbLPVGAColor (2), or vbLPColor (3). They choose among the images of an icon file; every DVX picture is a single bitmap, so, as for a bitmap in Visual Basic, they are checked and otherwise make no difference. A value outside these, or a negative x or y, raises error 5.

    A Picture object has three properties, which can only be read:

      Property   Type      Description
       --------   ----      -----------
    @@ -4059,13 +4122,13 @@ End With

    Creates a control on the given form. typeName$ is the control type (e.g. "CommandButton", "Label", "TextBox"). ctrlName$ is the unique name for the new control. If a parentCtrlRef is supplied, the new control is nested inside it (useful for placing controls inside containers like HBox or Frame); the parent must be a container on the same form, otherwise a runtime error (CreateControl: invalid parent) is raised.

    SetEvent

    SETEVENT ctrlRef, eventName$, handlerName$
    -

    Wires an arbitrary SUB as the handler for an event of a control, a form (Me or a form reference) or a menu item. The default event-handler convention (ControlName_EventName) is used automatically for controls defined in a .frm file; SetEvent is for controls you create dynamically or for reusing a single handler across multiple controls. A handler declared without parameters is called without the event's arguments.

    +

    Wires an arbitrary SUB as the handler for an event of a control, a form (Me or a form reference), or a menu item. The default event-handler convention (ControlName_EventName) is used automatically for controls defined in a .frm file; SetEvent is for controls you create dynamically or for reusing a single handler across multiple controls. A handler declared without parameters is called without the event's arguments.

    RemoveControl

    REMOVECONTROL formRef, ctrlName$

    Removes a control from a form. A later reference to the removed control raises a runtime error (unknown control), as does a reference to a control of a form that has been unloaded.

    LoadFormText

    SET formVar = LOADFORMTEXT(frmText$)
    -

    Loads a form from the text of a .frm file -- one a program made, read from a file or got from somewhere else -- and returns it, loaded but hidden like Load (its Load event runs; call Show). A form whose name is already loaded becomes another copy of its own, as with New. The form's controls fire events to procedures named after them in the running program (Gen1_Load, Go_Click), as for any form; a code section after the form's End is not compiled. Text that is not a form raises error 5 (Illegal function call). Form designers use it to show a .frm file being edited; Controls, CallByName and the designer.bas functions then read and change it.

    +

    Loads a form from the text of a .frm file -- one a program made, read from a file, or got from somewhere else -- and returns it, loaded but hidden like Load (its Load event runs; call Show). A form whose name is already loaded becomes another copy of its own, as with New. The form's controls fire events to procedures named after them in the running program (Gen1_Load, Go_Click), as for any form; a code section after the form's End is not compiled. Text that is not a form raises error 5 (Illegal function call). Form designers use it to show a .frm file being edited; Controls, CallByName, and the designer.bas functions then read and change it.

    Dim f As Object
     Dim n As Integer
     Dim t As String
    @@ -4201,7 +4264,7 @@ Set acct = Nothing              ' prints "Account for Scott closed"
    -

    The header is everything before the first line of code: blank lines, a VERSION line, a BEGIN ... END block and Attribute VB_... lines, in that order. All of it is optional, and a Visual Basic .cls file can be added as it is. The class's name comes from the Attribute VB_Name line; without one, the name is the file's name without .cls (ACCOUNT.CLS makes a class named ACCOUNT). The class name is the type name used in DIM ... AS, NEW, TypeOf and TYPENAME.

    +

    The header is everything before the first line of code: blank lines, a VERSION line, a BEGIN ... END block and Attribute VB_... lines, in that order. All of it is optional, and a Visual Basic .cls file can be added as it is. The class's name comes from the Attribute VB_Name line; without one, the name is the file's name without .cls (ACCOUNT.CLS makes a class named ACCOUNT). The class name is the type name used in DIM ... AS, NEW, TypeOf, and TYPENAME.

    What a Class Contains

    Outside its procedures, a class module may hold only declarations: member variables, CONSTs, OPTION and DEFtype statements, and Attribute lines. Statements that do something belong in a procedure, usually Class_Initialize. The members are:

      Member                             Written as                                 Used from outside as
    @@ -4213,7 +4276,7 @@ Option Explicit
    Write property Property Let name(..., value) obj.name = x Object property Property Set name(..., value) Set obj.name = someObject Friend procedure Friend Sub / Function / Property ... obj.name, by the program's own code -

    A Sub, Function or Property is Public unless it is marked Private; Private procedures can be called only by the class's own code. A Friend one, as in Visual Basic, can be called by the rest of the program through a variable declared as the class, but it is not part of the object's late-bound interface: through As Object or As Variant it raises error 438, and an Implements interface does not include it. Inside the class's procedures the member variables are used by name, with no SHARED needed, and each call works with the variables of the object it was called for. The class's code can also use the program's DIM SHARED (or PUBLIC) module variables, procedures and constants.

    +

    A Sub, Function, or Property is Public unless it is marked Private; Private procedures can be called only by the class's own code. A Friend one, as in Visual Basic, can be called by the rest of the program through a variable declared as the class, but it is not part of the object's late-bound interface: through As Object or As Variant it raises error 438, and an Implements interface does not include it. Inside the class's procedures the member variables are used by name, with no SHARED needed, and each call works with the variables of the object it was called for. The class's code can also use the program's DIM SHARED (or PUBLIC) module variables, procedures, and constants.

    Properties

    A property looks like a variable from outside, but reading or assigning it runs a procedure, so the class can check or calculate the value:

    Private mC As Double
    @@ -4259,16 +4322,16 @@ Set b = Nothing          ' last reference gone: Class_Terminate runs now<
         Me.X = Me.X + dx     ' uses Property Get X and Property Let X
     End Sub

    Using Objects

    -

    When the compiler knows an object's class -- a variable, array, parameter, member variable or FUNCTION declared As the class name or As Collection, New ClassName, or Me in a class module -- its members are found when the program is compiled (early binding), as in Visual Basic. A misspelled member, a Private one, a Sub used where a value is needed, a property used as a statement, assigning a property that has no Property Let or Set, reading one that has no Property Get, and the wrong number of arguments are then compile errors that point at the member. Calls made this way are also faster. A class's own members can be used anywhere in the class, also above the line that declares them, and through Me a class reaches its Private members as well (Me.mTotal).

    +

    When the compiler knows an object's class -- a variable, array, parameter, member variable, or FUNCTION declared As the class name or As Collection, New ClassName, or Me in a class module -- its members are found when the program is compiled (early binding), as in Visual Basic. A misspelled member, a Private one, a Sub used where a value is needed, a property used as a statement, assigning a property that has no Property Let or Set, reading one that has no Property Get, and the wrong number of arguments are then compile errors that point at the member. Calls made this way are also faster. A class's own members can be used anywhere in the class, also above the line that declares them, and through Me a class reaches its Private members as well (Me.mTotal).

    Through a variable declared As Object or As Variant the member is found by name when the program runs (late binding): obj.Deposit 5 looks for a Public member named Deposit in whatever object obj holds at that moment. A misspelled or Private member then raises error 438 when the line runs, and the wrong number of arguments raises error 450. Using a member of Nothing raises error 91 either way. An object of another class that reaches a variable of a class anyway (for example through a ByRef As Object argument) has its members found by name, as late binding would.

    -

    An argument that can never fit an object parameter of a SUB, FUNCTION or method is a compile error too: a number or text, or an object of another class than the parameter's (unless that class Implements it).

    +

    An argument that can never fit an object parameter of a SUB, FUNCTION, or method is a compile error too: a number or text, or an object of another class than the parameter's (unless that class Implements it).

    An object can be kept in a variable declared as its class name, As Object, or As Variant, in an array, in a TYPE field, in a Collection, or in another object's member variable. It can be passed to a SUB or FUNCTION and returned from a FUNCTION (assign the result with SET inside the function, Set MakeAccount = acct). TYPENAME gives an object's class name, and TypeOf obj Is ClassName tests it; Is tells whether two variables refer to the same object.

    -

    Arguments of a class's procedures follow the rules of SUB and FUNCTION calls when the object variable is declared as the class name: a parameter without BYVAL gets the caller's variable, so the procedure can change it, an omitted OPTIONAL argument gets its default, and a ParamArray collects the rest. When the object is reached through a variable declared As Object or As Variant, a variable or array element given as an argument is passed by reference too if the procedure's parameter is not BYVAL (Visual Basic's rule; other expressions go by value). What the procedure stores in such a parameter is converted to the type of the caller's variable (an Integer stays an Integer, as in Visual Basic). A TYPE field given as an argument (obj.Move rec.x, obj.Move pts(1).x) is passed by reference as well (a BYVAL parameter gets its value). The methods of forms and controls always take their arguments by value. An argument left out in the middle of a call (obj.Report a, , c) or at the end is an OPTIONAL parameter's default value, else its empty value: 0, "" and so on for a typed parameter, Missing for a Variant (ISMISSING is True). Leaving out a parameter that is not OPTIONAL raises error 449 (Argument not optional).

    +

    Arguments of a class's procedures follow the rules of SUB and FUNCTION calls when the object variable is declared as the class name: a parameter without BYVAL gets the caller's variable, so the procedure can change it, an omitted OPTIONAL argument gets its default, and a ParamArray collects the rest. When the object is reached through a variable declared As Object or As Variant, a variable or array element given as an argument is passed by reference too if the procedure's parameter is not BYVAL (Visual Basic's rule; other expressions go by value). What the procedure stores in such a parameter is converted to the type of the caller's variable (an Integer stays an Integer, as in Visual Basic). A TYPE field given as an argument (obj.Move rec.x, obj.Move pts(1).x) is passed by reference as well (a BYVAL parameter gets its value). The methods of forms and controls always take their arguments by value. An argument left out in the middle of a call (obj.Report a, , c) or at the end is an OPTIONAL parameter's default value, else its empty value: 0, "", and so on for a typed parameter, Missing for a Variant (ISMISSING is True). Leaving out a parameter that is not OPTIONAL raises error 449 (Argument not optional).

    An object parameter is itself passed like any other parameter: without BYVAL, SET on the parameter changes the caller's variable (a SUB can make a new object for the caller); with BYVAL, the procedure gets its own reference to the same object, and SET on it does not affect the caller.

    CALLBYNAME

    result = CALLBYNAME(object, memberName$, callType [, arguments...])
     CALLBYNAME object, memberName$, callType [, arguments...]
    -

    Uses the member of an object whose name the program has in a string, as if it had been written after a dot: a form, a control, a class instance or a Collection. callType says what to do: vbGet reads a property (with arguments, a property that takes them), vbLet assigns it the last argument, vbSet assigns it an object (the last argument), vbMethod calls a method or function with the arguments. The function gives the property's value or the method's result (Empty for vbLet and vbSet); as a statement the result is dropped. Another callType raises error 5 (Illegal function call), as does vbLet or vbSet without a value; a member the object does not have raises the error the same member written after a dot would. A form designer's property window uses it to read and set the properties designer.bas lists for a control type.

    +

    Uses the member of an object whose name the program has in a string, as if it had been written after a dot: a form, a control, a class instance, or a Collection. callType says what to do: vbGet reads a property (with arguments, a property that takes them), vbLet assigns it the last argument, vbSet assigns it an object (the last argument), vbMethod calls a method or function with the arguments. The function gives the property's value or the method's result (Empty for vbLet and vbSet); as a statement the result is dropped. Another callType raises error 5 (Illegal function call), as does vbLet or vbSet without a value; a member the object does not have raises the error the same member written after a dot would. A form designer's property window uses it to read and set the properties designer.bas lists for a control type.

    Dim prop As String
     prop = "Caption"
     Print CallByName(Command1, prop, vbGet)       ' Command1.Caption
    @@ -4279,7 +4342,7 @@ Print CallByName(acct, "Balance", vbGet)      ' a class instance's pro
     

    A class can name one property or method as its default member, used when the object is written with parentheses directly after it. Put this line in the class, inside the procedure (as Visual Basic writes it) or on its own at the top of the class:

    Attribute Item.VB_UserMemId = 0

    With Item as the default member, obj(3) means obj.Item(3), and obj(3) = x calls Property Let Item. Collection's default member is Item, which is why c(1) and c("key") work. A call's result can be indexed directly, too: c(1)(0) is element 0 of the array item 1 holds, and Split(s, ",")(2) the third part.

    -

    Where a value is needed -- in arithmetic, a comparison, text, PRINT, a typed variable or a typed parameter -- an object stands for its default member's value, as in Visual Basic: with Value as the default member, total = obj + 1 reads obj.Value, and obj = 9 (without SET) stores 9 through it. A member variable can be the default member too (Attribute Label.VB_UserMemId = 0). An object without a default member used as a value raises error 13. The object name on its own still means the object itself wherever an object is expected: SET, Is, passing it to an object parameter, adding it to a Collection. Controls have default properties as well (Text1 means Text1.Text); see the control overview.

    +

    Where a value is needed -- in arithmetic, a comparison, text, PRINT, a typed variable, or a typed parameter -- an object stands for its default member's value, as in Visual Basic: with Value as the default member, total = obj + 1 reads obj.Value, and obj = 9 (without SET) stores 9 through it. A member variable can be the default member too (Attribute Label.VB_UserMemId = 0). An object without a default member used as a value raises error 13. The object name on its own still means the object itself wherever an object is expected: SET, Is, passing it to an object parameter, adding it to a Collection. Controls have default properties as well (Text1 means Text1.Text); see the control overview.

    WITH...END WITH

    WITH names an object or a TYPE record once for a block of statements; inside it, a name starting with a dot (.Value, .Add 3) is that object's member or that record's field:

    With acct
    @@ -4310,13 +4373,13 @@ NoMoney:
         Resume Next

    Rules and Limits

      -
    • A member's name cannot be a reserved word. Names such as Name, Show, Hide, Load, Print, Open, Close, Line, Input, Get, Put, Type, Error and Timer are keywords in DVX BASIC and cannot be used; Text, Caption, Value, Count, Item and Add are fine.
    • +
    • A member's name cannot be a reserved word. Names such as Name, Show, Hide, Load, Print, Open, Close, Line, Input, Get, Put, Type, Error, and Timer are keywords in DVX BASIC and cannot be used; Text, Caption, Value, Count, Item, and Add are fine.
    • SHARED and DIM SHARED cannot be used in a class module; a class's code reaches the program's module variables that are declared DIM SHARED or PUBLIC.
    • A STATIC variable in a class procedure is kept per object, as in Visual Basic: each object has its own copy.
    • A class has at most 65536 member variables, its procedures' STATIC variables included; more fails to compile with "Too many member variables in ClassName".
    • Class_Initialize and Class_Terminate take no parameters.
    • A class module cannot hold statements outside its procedures; the compiler reports "Only declarations are allowed outside procedures in a class module".
    • -
    • NEW, PROPERTY, PUBLIC, PRIVATE and TYPEOF are reserved words. VARIANT, OBJECT, COLLECTION, EMPTY, NULL, EACH and IN are not.
    • +
    • NEW, PROPERTY, PUBLIC, PRIVATE, and TYPEOF are reserved words. VARIANT, OBJECT, COLLECTION, EMPTY, NULL, EACH, and IN are not.

    See also: Events

    See also: Interfaces (Implements)

    @@ -4343,9 +4406,9 @@ Public Sub Start() Next RaiseEvent Done End Sub
    -

    An event may have parameters but no return value. Its parameters cannot be OPTIONAL or a PARAMARRAY, and an event is always public (Public Event and Event mean the same; Private Event is an error). RAISEEVENT can only be used inside the class's own procedures, with exactly the arguments the event declares. A parameter without BYVAL is passed by reference, so a handler can change the variable given for it -- the cancel flag above is how a handler asks the class to stop. This holds for a variable, an array element (a(i)) and a TYPE field (rec.f, arr(i).f), as in Visual Basic. When several handlers get the event they all get the same variable, so each sees what the one before it stored; every store is converted to the variable's or field's type at once, as an assignment would be. While the handlers run, the arrays whose elements (or elements' fields) were given are locked: REDIM of one of them (or ERASE of a dynamic one) raises error 10. An expression, or a variable in its own parentheses, given for a by-reference parameter is passed as one temporary value that every handler shares, as in Visual Basic: the variable itself does not change, but a handler sees what the one before it stored. An argument is converted to the type of a typed event parameter, with the rules of SUB calls (a variable of another type given by reference is a compile error).

    +

    An event may have parameters but no return value. Its parameters cannot be OPTIONAL or a PARAMARRAY, and an event is always public (Public Event and Event mean the same; Private Event is an error). RAISEEVENT can only be used inside the class's own procedures, with exactly the arguments the event declares. A parameter without BYVAL is passed by reference, so a handler can change the variable given for it -- the cancel flag above is how a handler asks the class to stop. This holds for a variable, an array element (a(i)), and a TYPE field (rec.f, arr(i).f), as in Visual Basic. When several handlers get the event they all get the same variable, so each sees what the one before it stored; every store is converted to the variable's or field's type at once, as an assignment would be. While the handlers run, the arrays whose elements (or elements' fields) were given are locked: REDIM of one of them (or ERASE of a dynamic one) raises error 10. An expression, or a variable in its own parentheses, given for a by-reference parameter is passed as one temporary value that every handler shares, as in Visual Basic: the variable itself does not change, but a handler sees what the one before it stored. An argument is converted to the type of a typed event parameter, with the rules of SUB calls (a variable of another type given by reference is a compile error).

    Handling Events

    -

    In a module, a form or another class module, declare a variable of the class WithEvents, outside any procedure, and write a SUB named variable_EventName with the same parameters as the event:

    +

    In a module, a form, or another class module, declare a variable of the class WithEvents, outside any procedure, and write a SUB named variable_EventName with the same parameters as the event:

    Dim WithEvents dl As Downloader
     
     Sub StartDownload()
    @@ -4386,10 +4449,10 @@ End Sub

    Rules

      -
    • WITHEVENTS goes with DIM, PUBLIC or PRIVATE at the top level of a module, form or class module (not inside a procedure), for a single variable declared as a class module's name. It cannot be an array and cannot use New: Set it to an object in code.
    • +
    • WITHEVENTS goes with DIM, PUBLIC, or PRIVATE at the top level of a module, form, or class module (not inside a procedure), for a single variable declared as a class module's name. It cannot be an array and cannot use New: Set it to an object in code.
    • A WithEvents variable is visible to the procedures of its module or form without SHARED, as the handlers and the code that sets it need; in a class module it is a member variable of each object.
    • -
    • Handlers are SUBs named variable_EventName in the same module, form or class module as the variable.
    • -
    • EVENT, RAISEEVENT, WITHEVENTS and IMPLEMENTS are not reserved words: they only have their meaning where a name follows them in those places, so a program may still use them as its own names.
    • +
    • Handlers are SUBs named variable_EventName in the same module, form, or class module as the variable.
    • +
    • EVENT, RAISEEVENT, WITHEVENTS, and IMPLEMENTS are not reserved words: they only have their meaning where a name follows them in those places, so a program may still use them as its own names.

    See also: Objects and Class Modules

    See also: Interfaces (Implements)

    @@ -4422,13 +4485,13 @@ s.Scale 2 Print s.Area ' runs Circle's IShape_Area Print TypeOf s Is IShape ' True Print TypeName(s) ' Circle -

    Through a variable declared as the interface, a member always means the interface's member, even when the class has a public member of the same name of its own. Through the class's own type, As Object or As Variant, the class's own public members are used. SET into a variable of the interface type accepts an object of any class that Implements it (and of the interface class itself); TypeOf obj Is Interface is True for them. A class can implement several interfaces with one IMPLEMENTS line for each. An argument left out of a call through the interface takes the default the interface's procedure declares, as in Visual Basic, even if the implementing procedure declares another. Friend procedures are not part of an interface.

    +

    Through a variable declared as the interface, a member always means the interface's member, even when the class has a public member of the same name of its own. Through the class's own type, As Object, or As Variant, the class's own public members are used. SET into a variable of the interface type accepts an object of any class that Implements it (and of the interface class itself); TypeOf obj Is Interface is True for them. A class can implement several interfaces with one IMPLEMENTS line for each. An argument left out of a call through the interface takes the default the interface's procedure declares, as in Visual Basic, even if the implementing procedure declares another. Friend procedures are not part of an interface.

    See also: Objects and Class Modules

    See also: Events

    Collection Object

    -

    A Collection is a ready-made object that holds a list of items of any kind -- numbers, text, objects -- in order. Items can be looked up by position or by a text key, added anywhere in the list and removed, and FOR EACH walks through them. The list grows as needed; there is no size to declare.

    +

    A Collection is a ready-made object that holds a list of items of any kind -- numbers, text, objects -- in order. Items can be looked up by position or by a text key, added anywhere in the list, and removed, and FOR EACH walks through them. The list grows as needed; there is no size to declare.

    Dim fruit As New Collection
     fruit.Add "Apple", "a"
     fruit.Add "Cherry", "c"
    @@ -4458,10 +4521,10 @@ fruit.Remove 1                  ' removes Banana, now the first item
    Errors
      Error   When
       -----   ----
    -  5       A key that is not in the Collection (Item, Remove, before or after), or Add given both before and after
    +  5       A key that is not in the Collection (Item, Remove, before, or after), or Add given both before and after
       9       A position that is not in the Collection
       13      A key that is not text
    -  438     A member other than Add, Remove, Item and Count, or assigning one, through As Object or As Variant
    +  438     A member other than Add, Remove, Item, and Count, or assigning one, through As Object or As Variant
       450     The wrong number of arguments, through As Object or As Variant
       457     Add with a key that is already in use

    See also: Objects and Class Modules

    @@ -4527,7 +4590,7 @@ first$ = SQLFieldText$(rs, 0)

    SQLError$

    Returns the last error message for the database.

    msg$ = SQLError$(db)
    -

    When a database file could not be opened, read or written, the message ends with the reason in parentheses, for example "unable to open database file (No such file or directory)". If another program, or another handle in your own program, is changing the same database at that moment, the message is "database is locked": try again once the other change is finished, and free result sets with SQLFreeResult as soon as you are done with them, because an open result set stops other handles from saving changes.

    +

    When a database file could not be opened, read, or written, the message ends with the reason in parentheses, for example "unable to open database file (No such file or directory)". If another program, or another handle in your own program, is changing the same database at that moment, the message is "database is locked": try again once the other change is finished, and free result sets with SQLFreeResult as soon as you are done with them, because an open result set stops other handles from saving changes.

    Complete SQL Example

    Dim db As Long
     Dim rs As Long
    @@ -4575,11 +4638,11 @@ If Clipboard.GetFormat(vbCFText) Then Text2.Text = Clipboard.GetText()

    -

    The sizes are in pixels, the unit of a form's Left, Top, Width and Height in DVX (Visual Basic uses twips there).

    +

    The sizes are in pixels, the unit of a form's Left, Top, Width, and Height in DVX (Visual Basic uses twips there).

    ' Put the form at the right edge, 40 pixels from the top
     Me.Left = Screen.Width - Me.Width - 40
     Me.Top  = 40
    @@ -4599,7 +4662,7 @@ Screen.MousePointer = vbDefault
    Sub mnuCascade_Click ()
         Screen.Arrange vbCascade
     End Sub
    -

    Any other property, method or value stops the program with an error. The QBasic statement SCREEN and function SCREEN(row, column) are unaffected: Screen followed by a dot is the object. A control or form of your own named Screen takes the name's place.

    +

    Any other property, method, or value stops the program with an error. The QBasic statement SCREEN and function SCREEN(row, column) are unaffected: Screen followed by a dot is the object. A control or form of your own named Screen takes the name's place.

    INI Functions

    @@ -4646,7 +4709,7 @@ IniWrite App.Config & "\app.ini", "Display", "FontS
      Constant   Value   Description
       --------   -----   -----------
       vbModal    1       Show form as modal dialog
    -

    Window State, Arrangement and Mouse Pointer

    +

    Window State, Arrangement, and Mouse Pointer

    A form's WindowState and Screen.Arrange (see Form and Screen Object); the MousePointer values are under Mouse Constants.

      Constant           Value   Description
       --------           -----   -----------
    @@ -4668,7 +4731,7 @@ IniWrite App.Config & "\app.ini", "Display", "FontS
       vbDirectory   16      Entry is a directory
       vbArchive     32      File has been modified since last backup

    Compare Constants

    -

    The compare argument of FILTER, INSTRREV, REPLACE$, SPLIT and STRCOMP.

    +

    The compare argument of FILTER, INSTRREV, REPLACE$, SPLIT, and STRCOMP.

      Constant             Value   Description
       --------             -----   -----------
       vbUseCompareOption   -1      Follow OPTION COMPARE (the default)
    @@ -4691,7 +4754,7 @@ IniWrite App.Config & "\app.ini", "Display", "FontS
       vbFirstFourDays        2       Week 1 is the first with four days in the new year
       vbFirstFullWeek        3       Week 1 is the first full week

    Drag and Drop Constants

    -

    The DragMode property, the Drag method and the State argument of DragOver.

    +

    The DragMode property, the Drag method, and the State argument of DragOver.

      Constant      Value   Description
       --------      -----   -----------
       vbManual      0       DragMode: the program starts drags with Drag
    @@ -4706,7 +4769,7 @@ IniWrite App.Config & "\app.ini", "Display", "FontS
       --------   -----   -----------
       vbCFText   1       Text, the format the DVX clipboard holds

    Mouse Constants

    -

    The Button argument of the MouseDown, MouseUp and MouseMove events (several bits when several buttons are down), and the values of MousePointer (controls, forms and Screen).

    +

    The Button argument of the MouseDown, MouseUp, and MouseMove events (several bits when several buttons are down), and the values of MousePointer (controls, forms, and Screen).

      Constant         Value   Description
       --------         -----   -----------
       vbLeftButton     1       The left button
    @@ -4829,30 +4892,32 @@ End Sub

    DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory. Each file is a small library of DECLARE LIBRARY wrappers around native DVX runtime functions. Add an include file to your project (the IDE's Add File command, or editing the .dbp with another tool) and the declared functions become available to your code.

      File           Provides
       ----           --------
    -  commdlg.bas    File Open/Save dialogs (DlgFileOpenIn$ and DlgFileSaveIn$ start in a folder you name), input dialogs, choice dialogs, save-prompt dialogs, the color dialog, message boxes with your own buttons (DlgButtons)
    +  commdlg.bas    File Open/Save dialogs (DlgFileOpenIn$ and DlgFileSaveIn$ start in a folder you name), input dialogs, choice dialogs, save-prompt dialogs, the color dialog, the font dialog (DlgFont), message boxes with your own buttons (DlgButtons)
       sql.bas        SQLite database access (SQLOpen, SQLQuery, SQLNext, SQLField$, etc.)
    -  comm.bas       Serial-port I/O (raw and packet) and optional encrypted links; port settings read back and changed, why a call failed, breaks, ring and line errors (SerOpen, SerSet, SerLastError, CommOpen, CommHandshakeStep, ...)
    -  conn.bas       Connections the way Terminal makes them: a serial port, a modem that dials, the secure link to the DVX host proxy or Telnet, shown on a Terminal control, with capture, answerback and file transfers (ConnCreate, ConnOpen, ConnPoll, ConnAttach, ConnSendFiles, ...)
    -  xfer.bas       File transfers with XMODEM, XMODEM-1K, YMODEM and ZMODEM, on a serial port or a connection, with their progress and questions (XferSendFile, XferStatus, XferBytes, XferAnswer, ...)
    +  comm.bas       Serial-port I/O (raw and packet) and optional encrypted links; port settings read back and changed, why a call failed, breaks, ring, and line errors (SerOpen, SerSet, SerLastError, CommOpen, CommHandshakeStep, ...)
    +  conn.bas       Connections the way Terminal makes them: a serial port, a modem that dials, the secure link to the DVX host proxy, or Telnet, shown on a Terminal control, with capture, answerback, and file transfers (ConnCreate, ConnOpen, ConnPoll, ConnAttach, ConnSendFiles, ...)
    +  xfer.bas       File transfers with XMODEM, XMODEM-1K, YMODEM, and ZMODEM, on a serial port or a connection, with their progress and questions (XferSendFile, XferStatus, XferBytes, XferAnswer, ...)
       prefs.bas      Settings files (INI): load one, read and change its settings, list the keys of a section, save it; DVX's own settings file, the screen mode it starts in, the names its choices are stored as and its default choices, and choices stored by names of your own (PrefsLoad, PrefsGet$, PrefsSet, PrefsKeyName$, PrefsSaveAs, PrefsDvxIni$, PrefsVideoWidth, PrefsChoiceLabel$, PrefsChoiceDefault, PrefsGetChoiceFrom, ...)
       resource.bas   Read and write DVX resource blocks (icons, text, binary data) in bundled files
       help.bas       Compile help source files and see the compiler's messages, and open the DVX help viewer, at a topic if you like (HelpCompile, HelpCompileMessages$, HelpView, HelpViewTopic; HelpView "" opens the DVX user guide)
    -  hlpfile.bas    Read compiled help files yourself, for a help viewer of your own: topics, table of contents, keyword index, full-text search, and each topic's headings, text, links and pictures; show a topic in a HelpPane control (HlpOpen, HlpTocTitle$, HlpIndexKeyword$, HlpSearch, HlpLoadTopic, ...)
    +  hlpfile.bas    Read compiled help files yourself, for a help viewer of your own: topics, table of contents, keyword index, full-text search, and each topic's headings, text, links, and pictures; show a topic in a HelpPane control (HlpOpen, HlpTocTitle$, HlpIndexKeyword$, HlpSearch, HlpLoadTopic, ...)
       net.bas        Network connections: TCP (TcpConnect, TcpListen, TcpSend, TcpRecv$, TcpPeer$, ...), UDP messages and name lookups, any bytes; the network settings and status (NetConfigLoad, NetConfigSet, NetConfigStore, NetRestart, NetStatus$, ...)
       print.bas      Printing: text with the page setup and pictures, with or without the Print dialog; a form; the Page Setup dialog; the printer and page settings; the printer ports (PrintModelLabel$, PrintSettingsStore, PrintPageSetting, PrintChoose, PrintForm, PrintPortStatus, ...)
       listhelp.bas   Lists and icon views you draw yourself (a UserControl, a PictureBox) that behave like ListBox and Program Manager: selection with Shift and Ctrl, the arrow keys in a list or a grid, Ctrl+A, Space, type-ahead, where a dragged item lands, and Program Manager's icon view rules -- its cells, the grid with a scroll bar, names on two lines, dragging and dropping (ListSelCreate, ListSelClick, ListSelNavigate, ListTypeAhead, ListIconMetric, ListIconLabel$, ...)
    -  security.bas   Security: random bytes for keys, a key exchange, a cipher, SHA-256 hashes, HMAC signatures, key making and a safe compare (SecRandom$, SecDhCreate, SecCipherCrypt$, SecSha256$, SecHmac$, SecHkdf$, ...)
    +  security.bas   Security: random bytes for keys, a key exchange, a cipher, SHA-256 hashes, HMAC signatures, key making, and a safe compare (SecRandom$, SecDhCreate, SecCipherCrypt$, SecSha256$, SecHmac$, SecHkdf$, ...)
       game.bas       Games: whether a key is being held down right now, forgetting held keys, frames FRAMEWAIT skipped, waiting for the screen's retrace (GameKeyDown, GameKeyClear, FrameMissed, GameWaitRetrace)
    -  joystick.bas   Joysticks: how many there are, their names, where each stick points and which buttons are pressed (JoyCount, JoyX, JoyY, JoyButton, ...); setting them up: calibration, dead zone, game port mode, keyboard joystick keys (JoySetCalibration, JoyCalibration, JoyRescan, JoyKeyIndex, JoySettingsStore, ...)
    -  sound.bas      Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...); sounds loaded once and played with volume, pan and pitch, sound made by the program as it goes, the FM synthesizer chip (SoundLoad, SoundPlay, SoundSetPitch, SoundCreate, SoundStreamOpen, SoundStreamWrite, FmAlloc, FmWrite, ...); DVX's own event sounds (SoundPlayEvent), master volume, sound driver and MIDI settings (SoundEventFile$, SoundSetMasterVolume, SoundSettingsStore, ...)
    -  music.bas      Music: play MOD, S3M, XM, IT and MIDI files, and play notes yourself (MusicLoad, MusicPlay, MusicStop, MusicNoteOn, MusicInfo$, ...); raw bytes to the MIDI port (MidiAcquire, MidiPut, MidiRelease, MidiPortInfo$)
    -  fileops.bas    Files and folders: folder listings for a file list, paths and names, the file dialogs' filter rule, attributes and the time a file was changed, sizes, free space and disk sizes, drive kinds, whether a folder can be written, the folder for scratch files, making a folder path, and copying, moving and deleting whole folders at once or as a job with progress, Cancel and questions (FileOpsList, FileOpsListRows$, FileOpsFilterMatch, FileOpsSetModified, FileOpsDriveType, FileOpsIsWritable, FileOpsMakePath, FileOpsJobCreate, FileOpsJobRunFor, ...)
    -  system.bas     What Control Panel changes, at once for the whole desktop: the screen mode, the system colors and themes, the wallpaper and the mouse (with DVX's limits and defaults, MOUSE_* and SYS_VIDEO_DEFAULT_*), and storing them in DVX's settings; the system font's character pictures; the mouse pointer and the Shift, Ctrl and Alt keys anywhere on the screen, moving the pointer, typing keys into the window in front, a picture of the whole screen, the memory (SysSetVideoMode, SysSetColor, SysLoadTheme, SysSetWallpaper, SysSetMouse, SysColorsStore, SysFontGlyphRow, SysMouseX, SysSendKey, SysScreenshot, SysMemoryFreeKb, ...)
    +  joystick.bas   Joysticks: how many there are, their names, where each stick points, and which buttons are pressed (JoyCount, JoyX, JoyY, JoyButton, ...); setting them up: calibration, dead zone, game port mode, keyboard joystick keys (JoySetCalibration, JoyCalibration, JoyRescan, JoyKeyIndex, JoySettingsStore, ...)
    +  sound.bas      Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...); sounds loaded once and played with volume, pan, and pitch, sound made by the program as it goes, the FM synthesizer chip (SoundLoad, SoundPlay, SoundSetPitch, SoundCreate, SoundStreamOpen, SoundStreamWrite, FmAlloc, FmWrite, ...); DVX's own event sounds (SoundPlayEvent), master volume, sound driver, and MIDI settings (SoundEventFile$, SoundSetMasterVolume, SoundSettingsStore, ...)
    +  music.bas      Music: play MOD, S3M, XM, IT, and MIDI files, and play notes yourself (MusicLoad, MusicPlay, MusicStop, MusicNoteOn, MusicInfo$, ...); raw bytes to the MIDI port (MidiAcquire, MidiPut, MidiRelease, MidiPortInfo$)
    +  fileops.bas    Files and folders: folder listings for a file list, paths and names, the file dialogs' filter rule, attributes and the time a file was changed, sizes, free space and disk sizes, drive kinds, whether a folder can be written, the folder for scratch files, making a folder path, and copying, moving, and deleting whole folders at once or as a job with progress, Cancel, and questions (FileOpsList, FileOpsListRows$, FileOpsFilterMatch, FileOpsSetModified, FileOpsDriveType, FileOpsIsWritable, FileOpsMakePath, FileOpsJobCreate, FileOpsJobRunFor, ...)
    +  system.bas     What Control Panel changes, at once for the whole desktop: the screen mode, the system colors and themes, the wallpaper, text smoothing, and the mouse (with DVX's limits and defaults, MOUSE_* and SYS_VIDEO_DEFAULT_*), and storing them in DVX's settings; the system font's character pictures; the mouse pointer and the Shift, Ctrl, and Alt keys anywhere on the screen, moving the pointer, typing keys into the window in front, a picture of the whole screen, and the memory (SysSetVideoMode, SysSetColor, SysLoadTheme, SysSetWallpaper, SysSetTextSmoothing, SysSetMouse, SysColorsStore, SysFontGlyphRow, SysMouseX, SysSendKey, SysScreenshot, SysMemoryFreeKb, ...)
    +  font.bas       Fonts: the font families DVX has, the sizes each comes in, whether one is fixed-pitch, and how wide text and how tall a line is in a font (FontFamilyCount, FontFamilyName$, FontFamilyFixed, FontSizeCount, FontSizeAt, FontNearestSize, FontTextWidth, FontLineHeight, FontAscent, FontRescan)
    +  richtext.bas   RTF without a control: an RTF document as plain text and plain text as RTF (RtfToText$, TextToRtf$); a RichEdit control reads and writes RTF itself (TextRTF, SelRTF, LoadFile, SaveFile)
       layout.bas     Control sizes in characters or in percent of the container instead of pixels (SizeChars, SizePercent)
    -  compiler.bas   The BASIC compiler: compile a project or files you give it, find compile errors in their files, list a program's procedures and the language's keywords, build an .app or a control file; put a project's UserControls on a form designer's toolbox; read source text as the compiler does (tokens, code or string or comment, renaming a control in code, WithEvents variables, class Events), F1 help topics; open, change and save project files and UserControl headers (CompSourceFromProject, CompSourceAdd, CompCompile, CompError$, CompMapFile$, CompProcList$, CompBuildApp, CompDesignControls, CompTokens$, CompRenameInCode$, CompHelpTopic$, ProjOpen, ProjSave, ProjCtlSetHeader$, ...)
    +  compiler.bas   The BASIC compiler: compile a project or files you give it, find compile errors in their files, list a program's procedures and the language's keywords, build an .app or a control file; put a project's UserControls on a form designer's toolbox; read source text as the compiler does (tokens, code or string or comment, renaming a control in code, WithEvents variables, class Events), F1 help topics; open, change, and save project files and UserControl headers (CompSourceFromProject, CompSourceAdd, CompCompile, CompError$, CompMapFile$, CompProcList$, CompBuildApp, CompDesignControls, CompTokens$, CompRenameInCode$, CompHelpTopic$, ProjOpen, ProjSave, ProjCtlSetHeader$, ...)
       debug.bas      Run a compiled program inside yours and debug it: breakpoints, stepping, its variables, expressions, Immediate-window lines, its PRINT output (RunCreate, RunStart, RunPoll, DbgSetBreakpoints, DbgStepOver, DbgLocals$, DbgEval$, DbgExec$, ...)
    -  designer.bas   Form designers: the control types, whether each holds other controls and can be placed, the names of new controls, default events, toolbox tooltips and icons, and each type's properties (with their types and value names), methods and events (with each event's parameters); the names in .frm and class module text; .frm files as data to read, edit and write, controls copied and pasted as text, and a loaded form written back as .frm text (CtlTypeCount, CtlTypeName$, CtlTypeFlags, CtlPropCount, CtlPropName$, CtlPropType, CtlEventList$, CtlEventParams$, CtlTypeEventParams$, FrmFormName$, FrmControlNames$, ClsClassName$, FrmOpen, FrmText$, FrmCtlAdd, FrmCtlText$, FrmCtlPaste, FrmMenuAdd, FormText$, ...)
    -  shell.bas      DVX itself, everything Program Manager and Task Manager do: start programs and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close or end it), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, this program's settings folder, the DVX log; Program Manager's program groups with their items, shortcut keys and icons (ShellRun, ShellRunProgram, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellConfigFile$, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...)
    + designer.bas Form designers: the control types, whether each holds other controls and can be placed, the names of new controls, default events, toolbox tooltips and icons, and each type's properties (with their types and value names), methods, and events (with each event's parameters); the names in .frm and class module text; .frm files as data to read, edit, and write, controls copied and pasted as text, and a loaded form written back as .frm text (CtlTypeCount, CtlTypeName$, CtlTypeFlags, CtlPropCount, CtlPropName$, CtlPropType, CtlEventList$, CtlEventParams$, CtlTypeEventParams$, FrmFormName$, FrmControlNames$, ClsClassName$, FrmOpen, FrmText$, FrmCtlAdd, FrmCtlText$, FrmCtlPaste, FrmMenuAdd, FormText$, ...) + shell.bas DVX itself, everything Program Manager and Task Manager do: start programs and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close, or end it), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, this program's settings folder, the DVX log; Program Manager's program groups with their items, shortcut keys, and icons (ShellRun, ShellRunProgram, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellConfigFile$, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...)

    Using an Include Library

    ' With commdlg.bas added to the project
     Dim path As String
    @@ -4873,7 +4938,7 @@ Else
     End If
     On Error GoTo 0

    Message Boxes with Your Own Buttons

    -

    MsgBox offers fixed sets of buttons (OK, Yes and No, ...). With commdlg.bas in the project, DlgButtons(title, message, buttons, icon) shows a message box with the buttons you list, separated by |, with & before each button's access key. It returns the index of the button chosen: 0 for the first, which Enter presses. Esc and closing the box choose the last button, so make the last one the choice that changes nothing. icon is vbQuestion, vbInformation, vbExclamation, vbCritical or 0. Up to six buttons; -1 is returned when the list is empty or longer.

    +

    MsgBox offers fixed sets of buttons (OK, Yes and No, ...). With commdlg.bas in the project, DlgButtons(title, message, buttons, icon) shows a message box with the buttons you list, separated by |, with & before each button's access key. It returns the index of the button chosen: 0 for the first, which Enter presses. Esc and closing the box choose the last button, so make the last one the choice that changes nothing. icon is vbQuestion, vbInformation, vbExclamation, vbCritical, or 0. Up to six buttons; -1 is returned when the list is empty or longer.

    Select Case DlgButtons("Copy", "notes.txt already exists. Replace it?", "&Yes|&No|Yes to &All|&Cancel", vbQuestion)
         Case 0
             ' replace this one
    @@ -4883,13 +4948,29 @@ On Error GoTo 0
    ' keep it (No), or stop (Cancel) End Select

    Color Dialog

    -

    With commdlg.bas in the project, DlgColor(title, defaultColor) shows the color dialog: a palette of basic colors, Red, Green and Blue sliders with number boxes, and a preview. It starts at defaultColor and returns the color chosen, in the form RGB makes and ForeColor and BackColor use, or -1 when the user cancels.

    +

    With commdlg.bas in the project, DlgColor(title, defaultColor) shows the color dialog: a palette of basic colors, Red, Green, and Blue sliders with number boxes, and a preview. It starts at defaultColor and returns the color chosen, in the form RGB makes and ForeColor and BackColor use, or -1 when the user cancels.

    Dim c As Long
     c = DlgColor("Text Color", Label1.ForeColor)
     
     If c <> -1 Then
         Label1.ForeColor = c
     End If
    +

    Font Dialog

    +

    With commdlg.bas in the project, DlgFont(title, family, size, style, flags) shows the font dialog: the font families, the styles and sizes each comes in, Underline and Strikeout boxes, and a sample. It starts at the font named by family, size (in pixels), and style, and returns True when the user chooses OK. DlgFontName$(), DlgFontSize(), and DlgFontStyle() then give the font chosen; after Cancel they give the font the dialog started at. style adds up FONT_BOLD, FONT_ITALIC, FONT_UNDERLINE, and FONT_STRIKE, the constants of font.bas (add it to the project too, or use 1, 2, 4, and 8). With flags DLG_FONT_FIXED the dialog lists only the fixed-pitch families, for text that must line up in columns; with 0 it lists them all. The dialog's Licence button shows the chosen font's notice and licence. The answers fit a control's or a form's Font properties (see Fonts).

    +
    ' With commdlg.bas and font.bas added to the project
    +Dim style As Long
    +
    +If Label1.FontBold Then style = style + FONT_BOLD
    +If Label1.FontItalic Then style = style + FONT_ITALIC
    +
    +If DlgFont("Label Font", Label1.FontName, Label1.FontSize, style, 0) Then
    +    Label1.FontName = DlgFontName$()
    +    Label1.FontSize = DlgFontSize()
    +    Label1.FontBold = (DlgFontStyle() And FONT_BOLD) <> 0
    +    Label1.FontItalic = (DlgFontStyle() And FONT_ITALIC) <> 0
    +    Label1.FontUnderline = (DlgFontStyle() And FONT_UNDERLINE) <> 0
    +    Label1.FontStrikethru = (DlgFontStyle() And FONT_STRIKE) <> 0
    +End If

    Number Input Dialog

    With commdlg.bas in the project, DlgIntInput(title, prompt, default, min, max) asks for a whole number with a spin box, and DlgIntInputStep(title, prompt, default, min, max, step) does the same with the arrows moving by step. Both return the number entered, or default when the user cancels; DlgInputCancelled() (True after a Cancel, False after OK) tells the two apart, as it does for DlgInputBox$.

    n = DlgIntInputStep("Zoom", "Zoom in percent:", 100, 25, 400, 25)
    @@ -4898,7 +4979,7 @@ If Not DlgInputCancelled() Then
         SetZoom n
     End If

    Resources in Files

    -

    With resource.bas in the project, a program reads and writes the named resources DVX keeps at the end of program, widget and library files: icons, text and any bytes. ResOpen, ResCount, ResName$, ResType and ResSize list them; ResGetText$ and ResAddText read and write text; ResAddFile and ResExtract copy a resource from or to a file; ResRemove takes one out. ResGetData$(file, name) gives every byte of a resource as a String (CHR$(0) included), and ResAddData file, name, type, bytes adds or replaces a resource of any bytes (type RES_TYPE_BINARY, RES_TYPE_TEXT or RES_TYPE_ICON). RES_NAME_ICON ("icon32") and RES_NAME_WIDGET_ICON ("icon24") are the names DVX gives a program's and a widget's icon; a name is at most RES_NAME_MAX - 1 characters.

    +

    With resource.bas in the project, a program reads and writes the named resources DVX keeps at the end of program, widget, and library files: icons, text, and any bytes. ResOpen, ResCount, ResName$, ResType, and ResSize list them; ResGetText$ and ResAddText read and write text; ResAddFile and ResExtract copy a resource from or to a file; ResRemove takes one out. ResGetData$(file, name) gives every byte of a resource as a String (CHR$(0) included), and ResAddData file, name, type, bytes adds or replaces a resource of any bytes (type RES_TYPE_BINARY, RES_TYPE_TEXT, RES_TYPE_ICON, or RES_TYPE_FONT, a .DFN font the program carries; see Fonts). RES_NAME_ICON ("icon32") and RES_NAME_WIDGET_ICON ("icon24") are the names DVX gives a program's and a widget's icon; a name is at most RES_NAME_MAX - 1 characters.

    Dim settings As String
     settings = Chr$(1) + Chr$(0) + Chr$(255)
     
    @@ -4917,7 +4998,7 @@ Else
         Print "Handshake failed"
     End If

    After the handshake, data that arrives unencrypted is ignored unless that channel is opened for it with CommSetPlain link, channel, True.

    -

    CommHandshake waits until the key exchange is done. CommHandshakeStep(link, passphrase) does one step of it and returns at once, so a program can keep working: call it from a Timer until it returns something other than COMM_PENDING -- COMM_OK when the link is ready, else a COMM_ERR_ code. CommLastError(link) gives the COMM_ code of the last CommHandshake, CommHandshakeStep, CommSend, CommSetPlain or CommPoll, which tells a wrong passphrase (COMM_ERR_AUTH) from a line that does not answer (COMM_ERR_TIMEOUT). CommDropped(link) counts arriving packets thrown away because they were tampered with or came unencrypted where that is refused. Channels are numbered 0 to COMM_MAX_CHANNEL.

    +

    CommHandshake waits until the key exchange is done. CommHandshakeStep(link, passphrase) does one step of it and returns at once, so a program can keep working: call it from a Timer until it returns something other than COMM_PENDING -- COMM_OK when the link is ready, else a COMM_ERR_ code. CommLastError(link) gives the COMM_ code of the last CommHandshake, CommHandshakeStep, CommSend, CommSetPlain, or CommPoll, which tells a wrong passphrase (COMM_ERR_AUTH) from a line that does not answer (COMM_ERR_TIMEOUT). CommDropped(link) counts arriving packets thrown away because they were tampered with or came unencrypted where that is refused. Channels are numbered 0 to COMM_MAX_CHANNEL.

    Sub Timer1_Timer ()
         Select Case CommHandshakeStep(link, txtPass.Text)
             Case COMM_PENDING
    @@ -4938,13 +5019,13 @@ End Sub
      Function                                            Description
       --------                                            -----------
       SerSet(com, baud, dataBits, parity, stopBits, handshake)  Change the settings of an open port (SerOpen's arguments). True when done.
    -  SerGetBaud(com), SerGetDataBits(com), SerGetParity$(com), SerGetStopBits(com), SerGetHandshake(com)  The settings of an open port; parity is "N", "E", "O", "M" or "S". When the port is not open the numbers are an SER_ERR_ code and the parity "".
    +  SerGetBaud(com), SerGetDataBits(com), SerGetParity$(com), SerGetStopBits(com), SerGetHandshake(com)  The settings of an open port; parity is "N", "E", "O", "M", or "S". When the port is not open the numbers are an SER_ERR_ code and the parity "".
       SerTxPending(com), SerFlushTx com                   Bytes written but not sent yet; throw them away.
       SerSetBreak com, state                              Send a break: True holds the line in the break state, False releases it (hold it a quarter to half a second).
       SerGetRi(com)                                       True while the other end rings.
       SerLineErrors(com)                                  Errors seen on received data, added up: SER_LINE_OVERRUN (data came faster than it was read), SER_LINE_PARITY, SER_LINE_FRAMING, SER_LINE_BREAK; 0 for none.

    Printing

    -

    With print.bas in the project, a program prints like the programs that come with DVX. PrintTextDialog shows the Print dialog (printer, copies, pages) and prints the text on pages laid out by the page setup: margins, a header and a footer with the document name, date and page number. PrintPageSetup shows the Page Setup dialog, which the user's choices are saved from for every program. PrintPictureDialog prints a picture file scaled to the page, on printer models that print pictures (PrintCanPrintPictures says whether the chosen one does). PrintText and PrintPicture print without asking first. Each returns True when it printed.

    +

    With print.bas in the project, a program prints like the programs that come with DVX. PrintTextDialog shows the Print dialog (printer, copies, pages) and prints the text on pages laid out by the page setup: margins, a header, and a footer with the document name, date, and page number. PrintPageSetup shows the Page Setup dialog, which the user's choices are saved from for every program. PrintPictureDialog prints a picture file scaled to the page, on printer models that print pictures (PrintCanPrintPictures says whether the chosen one does). PrintText and PrintPicture print without asking first. Each returns True when it printed.

    Sub mnuPrint_Click ()
         PrintTextDialog txtNotes.Text, "Notes"
     End Sub
    @@ -4958,7 +5039,7 @@ End Sub
    -------- ----------- PrintPageCount(text) How many pages text takes with the saved page setup and printer. PrintTextPages(text, title, firstPage, lastPage, copies) Print text without asking: those pages (0 and 0 for all), copies times. - PrintChoose(title, pageCount) Show the Print dialog only (the page range when pageCount is above 0). True when the user chose Print; then PrintChoiceDest, PrintChoiceFile$, PrintChoiceCopies, PrintChoiceFirst and PrintChoiceLast say what was chosen (pages 0 and 0: all). + PrintChoose(title, pageCount) Show the Print dialog only (the page range when pageCount is above 0). True when the user chose Print; then PrintChoiceDest, PrintChoiceFile$, PrintChoiceCopies, PrintChoiceFirst, and PrintChoiceLast say what was chosen (pages 0 and 0: all). PrintForm(formName) / PrintFormDialog(formName) Print what a loaded form shows, scaled to the page (a printer model that prints pictures); the second shows the Print dialog first. PrintPortPresent(port), PrintPortStatus(port) Whether printer port LPTn (1 to 3) exists, and its state: PRINT_STATUS_ values added up (PRINT_STATUS_PAPER_OUT, PRINT_STATUS_SELECTED, PRINT_STATUS_NOT_BUSY, ...). PrintStatusProblem$(status, afterWrite, where) What is wrong with a printer in that state, in words ("" when nothing is), for printer where such as "LPT1". @@ -4975,11 +5056,11 @@ End If
      Function                                            Description
       --------                                            -----------
       PrintPageSetting(prefs, field)                      A number of the page setup: "Lines" (lines per page, 0 = the printer's usual length), "Columns", "MarginTop", "MarginBottom", "MarginLeft", "MarginRight", "TabWidth", or "Wrap" (True: long lines wrap at a space); -1 for an unknown field.
    -  PrintPageText$(prefs, field)                        "Header" or "Footer". In them &f is the document name, &p the page, &n the number of pages, &d the date, &t the time, && an ampersand; &l, &c and &r put what follows left, centred or right.
    +  PrintPageText$(prefs, field)                        "Header" or "Footer". In them &f is the document name, &p the page, &n the number of pages, &d the date, &t the time, && an ampersand; &l, &c, and &r put what follows left, centred, or right.
       PrintPageSetupProblem$(lines, columns, marginTop, marginBottom, marginLeft, marginRight, tabWidth, wrap, header, footer)  "" when this page setup can be used, else why not.
       PrintPageSettingsStore(prefs, lines, columns, marginTop, marginBottom, marginLeft, marginRight, tabWidth, wrap, header, footer)  Put a page setup into the settings (then PrefsSave). False for one PrintPageSetupProblem$ objects to.
       PrintPageLines(prefs, model)                        Lines per page the setup gives on printer model.
    -

    The limits are CONSTs: PRINT_LINES_MIN to PRINT_LINES_MAX, PRINT_COLUMNS_MIN to PRINT_COLUMNS_MAX, PRINT_TAB_MAX, PRINT_COPIES_MAX, with the defaults PRINT_COLUMNS_DEFAULT, PRINT_MARGIN_TB_DEFAULT, PRINT_MARGIN_LR_DEFAULT and PRINT_TAB_DEFAULT.

    +

    The limits are CONSTs: PRINT_LINES_MIN to PRINT_LINES_MAX, PRINT_COLUMNS_MIN to PRINT_COLUMNS_MAX, PRINT_TAB_MAX, PRINT_COPIES_MAX, with the defaults PRINT_COLUMNS_DEFAULT, PRINT_MARGIN_TB_DEFAULT, PRINT_MARGIN_LR_DEFAULT, and PRINT_TAB_DEFAULT.

    A program can also change the printer settings the way the Control Panel's Printer page does. They are kept in DVX's settings file, so these calls take a handle from prefs.bas (add prefs.bas to the project too). Printer models and destinations are numbered from 0.

      Function                                       Description
       --------                                       -----------
    @@ -5009,11 +5090,11 @@ Sub cmdOK_Click ()
         PrefsClose prefs
     End Sub

    Files and Folders

    -

    With fileops.bas in the project, a program can show and change what is on the disks. FileOpsList(folder, filter) reads a folder once -- its folders and the files matching filter -- and returns a listing (a number; FileOpsListFree frees it). FileOpsListRows$ gives the whole listing as name, size, type and date columns, ready for a ListView's SetRows, so even a folder of hundreds of files fills the list at once; FileOpsListSort re-sorts it by name, size, type or date with folders first. FileOpsListName$, FileOpsListIsFolder and FileOpsListSize read single entries, and FileOpsListFind finds one by name.

    -

    Paths: FileOpsPathNormalize$ (the full path of an existing folder, or ""), FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (True for a path that is complete by itself, such as "C:\DOCS", False for one that counts from the current folder), FileOpsPathIsInside(path, folder) (True when path is folder itself or anywhere inside it: check it before copying or moving a folder, so it is never copied into itself), FileOpsPathSep$ (the character between folder names, "\" on DOS), FileOpsPathNative$ (a path written with that character, for showing it: "C:/DOCS/A.TXT" becomes "C:\DOCS\A.TXT"), FileOpsBaseName$ and FileOpsExtension$. Every one of them takes / and \ alike and gives back \. FileOpsFilterMatch(itemName, filter) tells whether a name matches a filter the way the file dialogs match it: wildcards * and ?, several patterns separated by ; such as "*.bmp;*.png", case not mattering.

    -

    Files: FileOpsMakeDir and FileOpsRename (FileOpsError$ says why one failed), FileOpsTempPath$ and FileOpsReplaceFile (save safely: write the new file to FileOpsTempPath$(path$), then FileOpsReplaceFile swaps it in; the old file comes back if that fails), FileOpsValidName(folder, itemName) (whether a name can be used for a new file or folder in folder, by the rules of its disk; FileOpsError$ says why not), FileOpsValidLongName and FileOpsValidShortName (the long-name or the 8.3 rules by themselves, whatever the disk takes), FileOpsShortPath$ (the short 8.3 form of an existing path, for an old program that cannot take long names or blanks), FileOpsMakePath (a folder and every missing folder above it, which MKDIR does not make), FileOpsIsFolder, FileOpsGetAttr and FileOpsSetAttr (the FILEOPS_ATTR_ bits), FileOpsModified$ and FileOpsSetModified (when a file was last changed, read and set in the form "YYYY-MM-DD HH:MM"; FileOpsSetModified returns False for a date that does not exist), FileOpsTreeSize with FileOpsTreeBytes, FileOpsTreeFiles and FileOpsTreeFolders (the size of a whole folder) and FileOpsFormatSize$.

    -

    Drives: FileOpsDriveList$ (the drive letters), FileOpsDriveType (FILEOPS_DRIVE_FIXED for a hard disk, FILEOPS_DRIVE_REMOVABLE for a floppy disk drive, FILEOPS_DRIVE_REMOTE for a network drive or a CD-ROM; asking never makes a drive spin up), FileOpsBootDrive$ (the drive the computer started from), FileOpsFreeBytes, FileOpsFreeKb and FileOpsTotalBytes (the size of the disk; right for disks past 2 GB too), FileOpsLongNames(path) (True when the disk holding path takes long file names: up to 255 characters, with spaces and several dots; otherwise names are 8.3). FileOpsIsWritable(folder) is False where files cannot be made (a CD-ROM, a write-protected floppy), and FileOpsReadOnlySystem() is True when DVX itself runs from such a disk, so settings cannot be saved in its folder. FileOpsTempDir$() is the folder for scratch files ("" when there is none).

    -

    FileOpsCopyTree, FileOpsMoveTree and FileOpsDeleteTree do a whole folder at once. For a long copy, move or delete the program stays usable when it runs the work as a job, a little at a time from a Timer: FileOpsJobCreate, then FileOpsJobAddSource for each file or folder, then FileOpsJobRunFor from the timer until it returns FILEOPS_STEP_DONE or FILEOPS_STEP_CANCELLED. When it returns FILEOPS_STEP_ASK (a file already exists, a file is read-only) or FILEOPS_STEP_ERROR, show FileOpsJobMessage$ and pass the user's choice to FileOpsJobAnswer. FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. Files copied from a CD-ROM are read-only; FileOpsJobClearAttr job, FILEOPS_ATTR_READONLY before the job runs makes every copy changeable (FILEOPS_ATTR_EDITABLE clears all four attribute bits). Whatever a program leaves open is freed when it ends.

    +

    With fileops.bas in the project, a program can show and change what is on the disks. FileOpsList(folder, filter) reads a folder once -- its folders and the files matching filter -- and returns a listing (a number; FileOpsListFree frees it). FileOpsListRows$ gives the whole listing as name, size, type, and date columns, ready for a ListView's SetRows, so even a folder of hundreds of files fills the list at once; FileOpsListSort re-sorts it by name, size, type, or date with folders first. FileOpsListName$, FileOpsListIsFolder, and FileOpsListSize read single entries, and FileOpsListFind finds one by name.

    +

    Paths: FileOpsPathNormalize$ (the full path of an existing folder, or ""), FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (True for a path that is complete by itself, such as "C:\DOCS", False for one that counts from the current folder), FileOpsPathIsInside(path, folder) (True when path is folder itself or anywhere inside it: check it before copying or moving a folder, so it is never copied into itself), FileOpsPathSep$ (the character between folder names, "\" on DOS), FileOpsPathNative$ (a path written with that character, for showing it: "C:/DOCS/A.TXT" becomes "C:\DOCS\A.TXT"), FileOpsBaseName$, and FileOpsExtension$. Every one of them takes / and \ alike and gives back \. FileOpsFilterMatch(itemName, filter) tells whether a name matches a filter the way the file dialogs match it: wildcards * and ?, several patterns separated by ; such as "*.bmp;*.png", case not mattering.

    +

    Files: FileOpsMakeDir and FileOpsRename (FileOpsError$ says why one failed), FileOpsTempPath$ and FileOpsReplaceFile (save safely: write the new file to FileOpsTempPath$(path$), then FileOpsReplaceFile swaps it in; the old file comes back if that fails), FileOpsValidName(folder, itemName) (whether a name can be used for a new file or folder in folder, by the rules of its disk; FileOpsError$ says why not), FileOpsValidLongName and FileOpsValidShortName (the long-name or the 8.3 rules by themselves, whatever the disk takes), FileOpsShortPath$ (the short 8.3 form of an existing path, for an old program that cannot take long names or blanks), FileOpsMakePath (a folder and every missing folder above it, which MKDIR does not make), FileOpsIsFolder, FileOpsGetAttr, and FileOpsSetAttr (the FILEOPS_ATTR_ bits), FileOpsModified$ and FileOpsSetModified (when a file was last changed, read and set in the form "YYYY-MM-DD HH:MM"; FileOpsSetModified returns False for a date that does not exist), FileOpsTreeSize with FileOpsTreeBytes, FileOpsTreeFiles, and FileOpsTreeFolders (the size of a whole folder), and FileOpsFormatSize$.

    +

    Drives: FileOpsDriveList$ (the drive letters), FileOpsDriveType (FILEOPS_DRIVE_FIXED for a hard disk, FILEOPS_DRIVE_REMOVABLE for a floppy disk drive, FILEOPS_DRIVE_REMOTE for a network drive or a CD-ROM; asking never makes a drive spin up), FileOpsBootDrive$ (the drive the computer started from), FileOpsFreeBytes, FileOpsFreeKb, and FileOpsTotalBytes (the size of the disk; right for disks past 2 GB too), FileOpsLongNames(path) (True when the disk holding path takes long file names: up to 255 characters, with spaces and several dots; otherwise names are 8.3). FileOpsIsWritable(folder) is False where files cannot be made (a CD-ROM, a write-protected floppy), and FileOpsReadOnlySystem() is True when DVX itself runs from such a disk, so settings cannot be saved in its folder. FileOpsTempDir$() is the folder for scratch files ("" when there is none).

    +

    FileOpsCopyTree, FileOpsMoveTree, and FileOpsDeleteTree do a whole folder at once. For a long copy, move, or delete the program stays usable when it runs the work as a job, a little at a time from a Timer: FileOpsJobCreate, then FileOpsJobAddSource for each file or folder, then FileOpsJobRunFor from the timer until it returns FILEOPS_STEP_DONE or FILEOPS_STEP_CANCELLED. When it returns FILEOPS_STEP_ASK (a file already exists, a file is read-only) or FILEOPS_STEP_ERROR, show FileOpsJobMessage$ and pass the user's choice to FileOpsJobAnswer. FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. Files copied from a CD-ROM are read-only; FileOpsJobClearAttr job, FILEOPS_ATTR_READONLY before the job runs makes every copy changeable (FILEOPS_ATTR_EDITABLE clears all four attribute bits). Whatever a program leaves open is freed when it ends.

    Dim job As Long
     
     Sub cmdCopy_Click ()
    @@ -5068,8 +5149,8 @@ Sub FillTaskList ()
     End Sub

    Command lines. ShellCommandProgram$(commandLine) and ShellCommandArgs$(commandLine) split a command line the way ShellRunCommand does: the program (the first word, or the text in double quotes when it starts with one) and the arguments after it. ShellArgCount(commandLine) and ShellArg$(commandLine, index) split any command line into its words (index from 0) by DVX's rule: words are separated by blanks, and a word with blanks in it is written in double quotes, which ShellArg$ leaves off ("" past the last word). ShellArgQuote$(text) writes one word that way: text in double quotes when it holds blanks, as it is otherwise, so a folder or file name with spaces in it reaches the program it is passed to whole (ShellRun "notepad.app", ShellArgQuote$(path$)). COMMAND$(n) gives this program's own arguments the same way.

    Keys for every program. HotkeyAdd(keyText) makes a key such as "Ctrl+Alt+E" work whichever program is in front and returns its id (0 when it is not a key, needs Ctrl or Alt, or another program uses it). The program asks for the keys pressed from a Timer: HotkeyNext() returns the id of the oldest one not asked for yet, 0 when there is none. HotkeyRemove id stops one; the rest stop when the program ends. HotkeyText$(keyText) writes a key the way DVX does ("ctrl+alt+e" gives "Ctrl+Alt+E"; "" when it is not a key), handy for checking what the user typed. ShellItemKeyValid(keyText) is Program Manager's rule for an item's shortcut: Ctrl together with Alt or Shift, so it never takes a key a program uses itself ("" for no shortcut is valid too).

    -

    This program. ShellAppPath$ is this program's own path (ShellRun ShellAppPath$(), "" starts another copy), and ShellInstanceCount says how many copies are running, for example to place each new window a little lower and to the right. ShellSetPriority(priority) gives this program more or less of the computer's time while other programs run too: SHELL_PRIORITY_LOW, SHELL_PRIORITY_NORMAL (what every program starts with), SHELL_PRIORITY_HIGH or any number between; ShellPriority() reads it. A program busy with long work leaves the others room at low priority. A program allows more than one copy when Project Properties has "Allow more than one copy to run at once" ticked. ShellConfigFile$(fileName) is the full path of fileName in this program's settings folder -- where DVX's own programs keep their settings, the same for every copy of the program -- and makes the folder when it is missing; keep a desktop's groups there with prefs.bas. ShellLog text writes a line to the DVX log. ShellSetDesktop program makes a program the desktop DVX starts with the next time ("" for Program Manager again); when the desktop ends, DVX ends. ShellSetWelcome program sets the program DVX starts after the desktop the next time it starts ("" for none).

    -

    Program groups. Program Manager keeps its groups in PROGMAN.INI; a desktop written in BASIC uses the same model and file format. ShellGroupsLoad(path) opens a groups file (a file not saved yet has the groups Main and Startup) and returns a handle, ShellGroupsSave(groups, path) writes it and ShellGroupsFree frees it. Groups and items are numbered from 0; group 0 is Installed Programs, which ShellGroupsScan(groups, skipPath) fills with every installed program and which is never saved. ShellGroupsChanged tells cheaply whether a program was installed or removed since. ShellGroupCount, ShellGroupName$, ShellGroupFind, ShellGroupAdd, ShellGroupRename, ShellGroupDelete and ShellGroupNameValid manage the groups. ShellItemCount and ShellItemAdd(groups, group, name, program) list and add items; ShellItemGet$ and ShellItemSet read and change an item's fields by name ("name", "program", "args", "folder", "icon", "key", "minimized", "col", "row", "description"); ShellItemDelete, ShellItemCopy, ShellItemMove, ShellItemReorder and ShellItemPlace rearrange them, ShellGroupArrange and ShellGroupPlaceUnplaced lay out an icon grid, and ShellItemMatches is Program Manager's search. ShellGroupsGet and ShellGroupsSet read and set the options ("view": SHELL_VIEW_LARGE, SHELL_VIEW_SMALL or SHELL_VIEW_LIST; "group", the group shown; "autoArrange", "minimizeOnRun", "restoreAlone"), and ShellGroupsHistory$ / ShellGroupsHistoryAdd keep the Run history (what ShellRunDialog$ takes). ShellItemIcon$(iconFile, program) is the file an item's icon comes from -- its icon file, else its program or picture, else the program that opens the document -- ready for LOADPICTURE.

    +

    This program. ShellAppPath$ is this program's own path (ShellRun ShellAppPath$(), "" starts another copy), and ShellInstanceCount says how many copies are running, for example to place each new window a little lower and to the right. ShellSetPriority(priority) gives this program more or less of the computer's time while other programs run too: SHELL_PRIORITY_LOW, SHELL_PRIORITY_NORMAL (what every program starts with), SHELL_PRIORITY_HIGH, or any number between; ShellPriority() reads it. A program busy with long work leaves the others room at low priority. A program allows more than one copy when Project Properties has "Allow more than one copy to run at once" ticked. ShellConfigFile$(fileName) is the full path of fileName in this program's settings folder -- where DVX's own programs keep their settings, the same for every copy of the program -- and makes the folder when it is missing; keep a desktop's groups there with prefs.bas. ShellLog text writes a line to the DVX log. ShellSetDesktop program makes a program the desktop DVX starts with the next time ("" for Program Manager again); when the desktop ends, DVX ends. ShellSetWelcome program sets the program DVX starts after the desktop the next time it starts ("" for none).

    +

    Program groups. Program Manager keeps its groups in PROGMAN.INI; a desktop written in BASIC uses the same model and file format. ShellGroupsLoad(path) opens a groups file (a file not saved yet has the groups Main and Startup) and returns a handle, ShellGroupsSave(groups, path) writes it, and ShellGroupsFree frees it. Groups and items are numbered from 0; group 0 is Installed Programs, which ShellGroupsScan(groups, skipPath) fills with every installed program and which is never saved. ShellGroupsChanged tells cheaply whether a program was installed or removed since. ShellGroupCount, ShellGroupName$, ShellGroupFind, ShellGroupAdd, ShellGroupRename, ShellGroupDelete, and ShellGroupNameValid manage the groups. ShellItemCount and ShellItemAdd(groups, group, name, program) list and add items; ShellItemGet$ and ShellItemSet read and change an item's fields by name ("name", "program", "args", "folder", "icon", "key", "minimized", "col", "row", "description"); ShellItemDelete, ShellItemCopy, ShellItemMove, ShellItemReorder, and ShellItemPlace rearrange them, ShellGroupArrange and ShellGroupPlaceUnplaced lay out an icon grid, and ShellItemMatches is Program Manager's search. ShellGroupsGet and ShellGroupsSet read and set the options ("view": SHELL_VIEW_LARGE, SHELL_VIEW_SMALL, or SHELL_VIEW_LIST; "group", the group shown; "autoArrange", "minimizeOnRun", "restoreAlone"), and ShellGroupsHistory$ / ShellGroupsHistoryAdd keep the Run history (what ShellRunDialog$ takes). ShellItemIcon$(iconFile, program) is the file an item's icon comes from -- its icon file, else its program or picture, else the program that opens the document -- ready for LOADPICTURE.

    Dim groups As Long, main As Long, i As Long
     groups = ShellGroupsLoad(ShellConfigFile$("groups.ini"))
     main = ShellGroupFind(groups, "Main")
    @@ -5099,14 +5180,15 @@ End Sub
    End Sub

    Changing DVX's Settings

    With system.bas in the project, a program changes what Control Panel changes, at once for the whole desktop. Add prefs.bas too: the settings are saved through a prefs.bas handle.

    -

    Screen mode: SysVideoModeCount() says how many modes the display offers (numbered from 0), and SysVideoModeWidth, SysVideoModeHeight and SysVideoModeBpp (bits per pixel) describe each. SysSetVideoMode(wide, high, bpp) changes the mode and keeps every window; it returns False when the mode could not be set (the old one stays), also while a program has the whole screen to itself. A mode the monitor cannot show leaves the user unable to answer, so do as Control Panel does: ask "Keep this mode?" and go back to the old mode by itself after some seconds. Screen.Width, Screen.Height and Screen.BitsPerPixel tell the mode in use, and SYS_VIDEO_DEFAULT_WIDTH, SYS_VIDEO_DEFAULT_HEIGHT and SYS_VIDEO_DEFAULT_BPP the mode DVX starts in when its settings name none. SysScreenshot(path) saves the whole screen as a picture file (BMP when path ends in .bmp, PNG otherwise) and returns True when it was written.

    +

    Screen mode: SysVideoModeCount() says how many modes the display offers (numbered from 0), and SysVideoModeWidth, SysVideoModeHeight, and SysVideoModeBpp (bits per pixel) describe each. SysSetVideoMode(wide, high, bpp) changes the mode and keeps every window; it returns False when the mode could not be set (the old one stays), also while a program has the whole screen to itself. A mode the monitor cannot show leaves the user unable to answer, so do as Control Panel does: ask "Keep this mode?" and go back to the old mode by itself after some seconds. Screen.Width, Screen.Height, and Screen.BitsPerPixel tell the mode in use, and SYS_VIDEO_DEFAULT_WIDTH, SYS_VIDEO_DEFAULT_HEIGHT, and SYS_VIDEO_DEFAULT_BPP the mode DVX starts in when its settings name none. SysScreenshot(path) saves the whole screen as a picture file (BMP when path ends in .bmp, PNG otherwise) and returns True when it was written.

    Colors: the system colors are numbered from 0 to SysColorCount() - 1. SysColorLabel$(id) is the text to show for one ("Desktop", "Window Face" ...), SysColorName$(id) the name it is stored as, and SysColorId(colorName) the id of a name (-1 for none) -- to draw in the user's colors, for example SysGetColor(SysColorId("desktop")). SysGetColor and SysSetColor read and set a color as an RGB() value; each SysSetColor redraws the whole screen. SysResetColors goes back to DVX's own colors. SysLoadTheme(path) and SysSaveTheme(path) read and write a theme file (.THM, kept in CONFIG\THEMES); both return True when they worked.

    -

    Wallpaper: SysSetWallpaper(path) shows a BMP, JPG, PNG or GIF image behind the desktop ("" for none) and returns False when the image could not be loaded (the old one stays). SysSetWallpaperMode mode says how it fills the screen, a choice of PREFS_CHOICE_WALLPAPER (Stretch, Tile or Center; see Settings Files). SysWallpaper$() and SysWallpaperMode() tell what is in use.

    -

    Mouse: SysSetMouse wheelDir, dblClickMs, accel, mickeys, wheelStep applies a whole mouse setup at once: the scroll wheel's direction (1 or -1, the values of PREFS_CHOICE_WHEEL), the double-click time in milliseconds, the acceleration (a value of PREFS_CHOICE_ACCEL), how far the mouse moves for 8 pixels of pointer movement (8 is normal, higher is slower) and the lines scrolled per wheel notch (1 to 10). An accel or mickeys of 0 leaves that one as it is. SysMouseWheelDir, SysMouseDoubleClick, SysMouseAccel, SysMouseMickeys and SysMouseWheelStep read the setup in effect. MOUSE_WHEEL_STEP_MIN and MOUSE_WHEEL_STEP_MAX are the limits of the wheel step, and MOUSE_WHEEL_STEP_DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS and MOUSE_SPEED_DEFAULT are DVX's defaults (what a Default button puts back).

    -

    The pointer and the keyboard: SysMouseX() and SysMouseY() are where the mouse pointer is on the screen, and SysMouseButtons() the buttons held (vbLeftButton, vbRightButton and vbMiddleButton bits) -- also while the pointer is over another program's window, where your MouseMove events do not see it. SysSetMousePos x, y moves the pointer (it is kept on the screen). SysShiftState() is the Shift, Ctrl and Alt keys held now (vbShiftMask, vbCtrlMask and vbAltMask bits). SysSendKey(keyCode, shift) types a key into the window that has the focus as if the user pressed it, like SendKeys in other BASICs: keyCode as a KeyDown event gets it -- a character code such as ASC("a") or 13 for Enter, or a vbKey constant such as vbKeyF1 -- and shift the vbShiftMask, vbCtrlMask and vbAltMask bits held with it (SysSendKey ASC("s"), vbCtrlMask is Ctrl+S). The keys arrive in order, one at a time, once your code has returned to DVX; up to 64 can wait, and SysSendKey returns False when that many are waiting. A program cannot press the mouse buttons: to "click" a control, call its event procedure or set its value.

    +

    Wallpaper: SysSetWallpaper(path) shows a BMP, JPG, PNG, or GIF image behind the desktop ("" for none) and returns False when the image could not be loaded (the old one stays). SysSetWallpaperMode mode says how it fills the screen, a choice of PREFS_CHOICE_WALLPAPER (Stretch, Tile, or Center; see Settings Files). SysWallpaper$() and SysWallpaperMode() tell what is in use.

    +

    Text: SysSetTextSmoothing True smooths the edges of text and SysSetTextSmoothing False stops it; SysTextSmoothing() tells which the user chose. Text is smoothed only when the screen shows more than 256 colors, and only in the fonts and sizes that come smoothed. Every window is drawn again when it changes.

    +

    Mouse: SysSetMouse wheelDir, dblClickMs, accel, mickeys, wheelStep applies a whole mouse setup at once: the scroll wheel's direction (1 or -1, the values of PREFS_CHOICE_WHEEL), the double-click time in milliseconds, the acceleration (a value of PREFS_CHOICE_ACCEL), how far the mouse moves for 8 pixels of pointer movement (8 is normal, higher is slower), and the lines scrolled per wheel notch (1 to 10). An accel or mickeys of 0 leaves that one as it is. SysMouseWheelDir, SysMouseDoubleClick, SysMouseAccel, SysMouseMickeys, and SysMouseWheelStep read the setup in effect. MOUSE_WHEEL_STEP_MIN and MOUSE_WHEEL_STEP_MAX are the limits of the wheel step, and MOUSE_WHEEL_STEP_DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS, and MOUSE_SPEED_DEFAULT are DVX's defaults (what a Default button puts back).

    +

    The pointer and the keyboard: SysMouseX() and SysMouseY() are where the mouse pointer is on the screen, and SysMouseButtons() the buttons held (vbLeftButton, vbRightButton, and vbMiddleButton bits) -- also while the pointer is over another program's window, where your MouseMove events do not see it. SysSetMousePos x, y moves the pointer (it is kept on the screen). SysShiftState() is the Shift, Ctrl, and Alt keys held now (vbShiftMask, vbCtrlMask, and vbAltMask bits). SysSendKey(keyCode, shift) types a key into the window that has the focus as if the user pressed it, like SendKeys in other BASICs: keyCode as a KeyDown event gets it -- a character code such as ASC("a") or 13 for Enter, or a vbKey constant such as vbKeyF1 -- and shift the vbShiftMask, vbCtrlMask, and vbAltMask bits held with it (SysSendKey ASC("s"), vbCtrlMask is Ctrl+S). The keys arrive in order, one at a time, once your code has returned to DVX; up to 64 can wait, and SysSendKey returns False when that many are waiting. A program cannot press the mouse buttons: to "click" a control, call its event procedure or set its value.

    Memory: SysMemoryTotalKb() is the computer's memory in KB and SysMemoryFreeKb() how much of it is free (-1 when it cannot be told).

    System font: SysFontWidth() and SysFontHeight() are the size of every character of the system font in pixels, and SysFontFirstChar() to SysFontFirstChar() + SysFontCharCount() - 1 the character codes it has. SysFontGlyphRow(code, row) is one row of a character's picture: a bit per pixel, &H80 the leftmost -- for drawing text dot by dot, enlarged or in a font editor of your own.

    -

    Saving: SysColorsStore, SysWallpaperStore and SysMouseStore write the colors, the wallpaper and the mouse setup into a prefs.bas handle, in the sections DVX reads when it starts; PrefsSetVideoMode does the same for a screen mode. Store them in PrefsLoad(PrefsDvxIni$()) and save it, and DVX starts with them next time. SysColorsLoad, SysWallpaperLoad and SysMouseLoad apply what a handle holds. A PrefsCreate handle filled with the Store calls is a snapshot in memory: load it again to undo every change, the way Cancel works in Control Panel.

    +

    Saving: SysColorsStore, SysWallpaperStore, SysFontsStore, and SysMouseStore write the colors, the wallpaper, text smoothing, and the mouse setup into a prefs.bas handle, in the sections DVX reads when it starts; PrefsSetVideoMode does the same for a screen mode. Store them in PrefsLoad(PrefsDvxIni$()) and save it, and DVX starts with them next time. SysColorsLoad, SysWallpaperLoad, SysFontsLoad, and SysMouseLoad apply what a handle holds. A PrefsCreate handle filled with the Store calls is a snapshot in memory: load it again to undo every change, the way Cancel works in Control Panel.

    Dim snap As Long
     Dim oldW As Long, oldH As Long, oldBpp As Long
     
    @@ -5114,6 +5196,7 @@ Sub Form1_Load ()
         snap = PrefsCreate()
         SysColorsStore snap
         SysWallpaperStore snap
    +    SysFontsStore snap
         SysMouseStore snap
         oldW = Screen.Width
         oldH = Screen.Height
    @@ -5125,6 +5208,7 @@ Sub cmdOK_Click ()
         ini = PrefsLoad(PrefsDvxIni$())
         SysColorsStore ini
         SysWallpaperStore ini
    +    SysFontsStore ini
         SysMouseStore ini
         PrefsSetVideoMode ini, Screen.Width, Screen.Height, Screen.BitsPerPixel
         If Not PrefsSave(ini) Then MsgBox "The settings could not be saved."
    @@ -5135,14 +5219,53 @@ End Sub
     Sub cmdCancel_Click ()
         SysColorsLoad snap
         SysWallpaperLoad snap
    +    SysFontsLoad snap
         SysMouseLoad snap
         If Screen.Width <> oldW Or Screen.Height <> oldH Or Screen.BitsPerPixel <> oldBpp Then
             SysSetVideoMode oldW, oldH, oldBpp
         End If
         Unload Me
     End Sub
    +

    Fonts

    +

    Besides the System font (the 8x16 font DVX draws everything with unless told otherwise), DVX comes with font families in its FONTS folder: families for text (sans-serif, serif, and fixed-pitch), display and handwriting families for headings, and pixel fonts. Which of them a computer has depends on how DVX was installed: Setup puts in none, the base set (DejaVu Sans, Sans Mono, and Serif), or all of them. Text in a family that is not installed is drawn in the System font, so a program that cares can look for the family first (FontFamilyCount and FontFamilyName$). With font.bas in the project a program can list them and measure text in them.

    +

    A family comes in up to four styles -- regular, bold, italic, and bold italic -- and in the pixel sizes it was made at. A font is always drawn at one of those sizes, never scaled: asking for another size gives the nearest one (FontNearestSize says which), and asking for a style a family has no face for makes it from the nearest face, bold by thickening and italic by slanting. Name a font by its family, size, and style on every call; a family name DVX does not have means the System font. Style is 0 for regular or FONT_BOLD + FONT_ITALIC; FONT_UNDERLINE and FONT_STRIKE are lines drawn with the text.

    +
    ' With font.bas added to the project: every family and its sizes
    +Dim i As Long
    +Dim j As Long
    +Dim sizes As String
    +
    +For i = 0 To FontFamilyCount() - 1
    +    sizes = ""
    +    For j = 0 To FontSizeCount(FontFamilyName$(i), 0) - 1
    +        sizes = sizes & " " & Str$(FontSizeAt(FontFamilyName$(i), 0, j))
    +    Next j
    +    Print FontFamilyName$(i); ":"; sizes
    +Next i
    +
    +' How wide a heading is in 24-pixel bold DejaVu Sans
    +Print FontTextWidth("DejaVu Sans", 24, FONT_BOLD, "Chapter One")
    +

    FontFamilyFixed tells a fixed-pitch family (every character as wide as every other, for columns and code) from a proportional one. FontLineHeight is the height of one line of text and FontAscent the distance from the top of a line to the baseline the letters sit on. FontRescan reads the FONTS folder again after fonts were added or removed. FontLicence$(family, size, style) gives the notice and licence that go with a font: where it came from, its copyright notices, and its licence text (empty for the System font).

    +

    A program can carry fonts of its own, so its text looks the same whatever fonts were installed. List the .DFN files in the project's Fonts (Project > Properties in the IDE): Make builds them into the program, and they are its fonts whenever it runs, from the IDE too. FontAddFile(path), FontAddData(label, bytes), and FontAddResources(file) add more while it runs. A program's fonts are its own -- other programs never see them -- and they go when it ends. Where a font it carries is of a family that is installed too, its sizes and styles come first and the installed family fills in the rest: a program that carries DejaVu Sans Bold at 36 pixels draws that size from its own font and every other size and style of DejaVu Sans from the FONTS folder. Fonts are made from TrueType, Type 1, and bitmap fonts with the SDK's mkfont, or copied from the FONTS folder; each .DFN carries its own licence.

    +
    ' With font.bas added to the project: a title in a font the program carries
    +If FontAddFile(App.Path + "\TITLE.DFN") Then
    +    lblTitle.FontName = "DejaVu Sans"
    +    lblTitle.FontSize = 36
    +    lblTitle.FontBold = True
    +End If
    +

    Every control and every form can have a font of its own, set with the properties FontName, FontSize (in pixels), FontBold, FontItalic, FontUnderline, and FontStrikethru, as in Visual Basic; they need no include file. A control that is given no font uses the font of the control it is in (a Frame, for example), else its form's, so setting a form's font changes every control on it that has none. Reading one of the properties gives the font the object draws with: a FontSize the family does not come in reads back as the nearest size it has, and a FontName DVX does not have reads back as "System". Setting one changes only that part of the font. Controls that edit text in columns (TextBox, TextArea, ComboBox, SpinButton, and Terminal) take only fixed-pitch families and show the System font in place of a proportional one. Print, TextWidth, TextHeight, PrintAccel, and TextWidthAccel on forms and picture boxes use the object's font. In the form designer, the Font row of the Properties window sets the six properties at once, and they are saved in the .frm file; DlgFont (see Font Dialog) lets the user choose a font while the program runs.

    +
    Sub Form1_Load ()
    +    Me.FontName = "DejaVu Sans"        ' every control without a font of its own
    +    Me.FontSize = 14
    +    lblTitle.FontSize = 24              ' the title larger, in DejaVu Sans too
    +    lblTitle.FontBold = True
    +    txtNotes.FontName = "DejaVu Sans Mono"
    +End Sub
    +

    Two controls mix fonts in one control. A RichLabel shows text built by calls -- SetFont, SetStyle, and SetColor choose how the text added next looks, AddText adds it -- in paragraphs aligned left, centered, or right. A RichEdit is a text box the user writes such text in, like a small word processor: the Sel properties (SelBold, SelFontName, SelColor, SelAlignment, ...) read and change the selection's look, TextRTF holds the whole text with its looks as RTF, and LoadFile and SaveFile read and write files. richtext.bas turns RTF into plain text and back without a control. See the controls' own pages for their properties.

    +
    Sub cmdBold_Click ()
    +    RichEdit1.SelBold = Not RichEdit1.SelBold
    +End Sub

    Held Keys for Games

    -

    With game.bas in the project, GameKeyDown(keyCode) tells whether a key is being held down right now, so a game can move something for as long as a key is held instead of waiting for key presses. It returns True only while one of the program's own windows is active. Name the key with one of the GAMEKEY_ constants (the arrow keys, Shift, Ctrl, Alt, Space, Enter, Esc and a few letters) or with its PC keyboard scan code (add &H80 for the grey keys between the typing keys and the number pad). A Timer polls it:

    +

    With game.bas in the project, GameKeyDown(keyCode) tells whether a key is being held down right now, so a game can move something for as long as a key is held instead of waiting for key presses. It returns True only while one of the program's own windows is active. Name the key with one of the GAMEKEY_ constants (the arrow keys, Shift, Ctrl, Alt, Space, Enter, Esc, and a few letters) or with its PC keyboard scan code (add &H80 for the grey keys between the typing keys and the number pad). A Timer polls it:

    Sub Timer1_Timer ()
         If GameKeyDown(GAMEKEY_LEFT) Then shipX = shipX - 2
         If GameKeyDown(GAMEKEY_RIGHT) Then shipX = shipX + 2
    @@ -5158,15 +5281,15 @@ End Sub
    FRAMEWAIT 60 Loop Until quitting

    Joysticks

    -

    With joystick.bas in the project, a program reads joysticks and game pads. JoyCount() says how many there are; they are numbered from 1, and so are their axes, buttons and hats. JoyX(joy) and JoyY(joy) say where the stick points, from -32768 (all the way left or up) to 32767 (all the way right or down), 0 in the middle; JoyAxis(joy, axis) reads any axis (3 and 4 are a throttle or rudder). JoyButton(joy, button) is True while the button is held down, and also once after a quick press that happened since the program last asked about that button, so a Timer never misses a shot. JoyHat(joy, hat) gives the direction a hat switch is pushed in degrees (0 is up, 90 right) or JOY_HAT_CENTERED. JoyName$(joy), JoyAxes(joy) and JoyButtons(joy) describe the joystick.

    +

    With joystick.bas in the project, a program reads joysticks and game pads. JoyCount() says how many there are; they are numbered from 1, and so are their axes, buttons, and hats. JoyX(joy) and JoyY(joy) say where the stick points, from -32768 (all the way left or up) to 32767 (all the way right or down), 0 in the middle; JoyAxis(joy, axis) reads any axis (3 and 4 are a throttle or rudder). JoyButton(joy, button) is True while the button is held down, and also once after a quick press that happened since the program last asked about that button, so a Timer never misses a shot. JoyHat(joy, hat) gives the direction a hat switch is pushed in degrees (0 is up, 90 right) or JOY_HAT_CENTERED. JoyName$(joy), JoyAxes(joy), and JoyButtons(joy) describe the joystick.

    Sub Timer1_Timer ()
         If JoyCount() = 0 Then Exit Sub
     
         shipX = shipX + JoyX(1) \ 8192
         If JoyButton(1, 1) Then FireShot
     End Sub
    -

    When no joystick is connected the keyboard stands in for one (the arrow keys, with Left Ctrl, Space, Left Shift and Z as buttons), and it only answers while one of the program's windows is active. Joysticks are calibrated, and the keyboard joystick's keys chosen, on the Control Panel's Game Controllers tab. QBasic's STICK and STRIG work too, without joystick.bas.

    -

    joystick.bas can also set joysticks up, as the Control Panel does. JoyHats(joy) counts the hats, JoyAxisPresent(joy, axis) is True when an axis answers, JoyConnected(joy) is False once a joystick stops answering, and JoyCanCalibrate(joy) is True for one with analog axes. For calibrating, JoyRawAxis(joy, axis) gives an axis's raw reading and JoyRawButtons(joy) the buttons held (1 for button 1, 2 for button 2, 4 for button 3, ... added up); JoySetCalibration(joy, axis, minV, centerV, maxV) takes the readings at one end, the middle and the other end, and JoyClearCalibration(joy, axis) lets the axis find its own range again. JoyDeadZone(joy) and JoySetDeadZone(joy, percent) are the dead zone round the middle (0 to 50 percent). JoyChoiceCount, JoyChoiceLabel$, JoyChoice and JoySetChoice list and change the game port's mode (JOY_CHOICE_GAMEPORT) and when the keyboard joystick stands in (JOY_CHOICE_FALLBACK); JoyKeyCount and JoyKeyName$(index) list the keys the keyboard joystick can use, and JoyKbdSlot(slot) and JoySetKbdSlot(slot, keyIndex) its keys for up, down, left, right and buttons 1 to 4 (slots 0 to 7). These lists are numbered from 0, so they fill a DropDown in order. JoyKeyIndex(keyName) gives a key's number by its name (-1 when unknown), JoyKeyCode(index) its raw key code, which GameKeyDown in game.bas takes, and JoyKeyNameOf$(rawCode) the name of a raw key code. JoyIsKeyboard(joy) is True for the keyboard standing in for a joystick, and JoyCalibration(joy, axis, part) reads back an axis's calibration: part JOY_CAL_MIN, JOY_CAL_CENTER or JOY_CAL_MAX of the readings set with JoySetCalibration, or of the range the axis found by itself (0 for a joystick that cannot be calibrated). JoySetting$(key, default) and JoySetSetting key, value reach any other joystick setting; JOY_KEY_POLL_HZ is the key of how often joysticks are read, JOY_POLL_HZ_MIN to JOY_POLL_HZ_MAX times a second (JOY_POLL_HZ_DEFAULT normally), and dead zones go up to JOY_DEADZONE_MAX percent (JOY_DEADZONE_DEFAULT normally). After a new game port mode, JoyRescan() looks for joysticks again; it returns the new count, or -1 while another program is using a joystick. Every change applies at once; JoySettingsStore handle puts them all into a settings handle from prefs.bas for the program to save, and JoySettingsLoad handle takes them back from one, to undo.

    +

    When no joystick is connected the keyboard stands in for one (the arrow keys, with Left Ctrl, Space, Left Shift, and Z as buttons), and it only answers while one of the program's windows is active. Joysticks are calibrated, and the keyboard joystick's keys chosen, on the Control Panel's Game Controllers tab. QBasic's STICK and STRIG work too, without joystick.bas.

    +

    joystick.bas can also set joysticks up, as the Control Panel does. JoyHats(joy) counts the hats, JoyAxisPresent(joy, axis) is True when an axis answers, JoyConnected(joy) is False once a joystick stops answering, and JoyCanCalibrate(joy) is True for one with analog axes. For calibrating, JoyRawAxis(joy, axis) gives an axis's raw reading and JoyRawButtons(joy) the buttons held (1 for button 1, 2 for button 2, 4 for button 3, ... added up); JoySetCalibration(joy, axis, minV, centerV, maxV) takes the readings at one end, the middle, and the other end, and JoyClearCalibration(joy, axis) lets the axis find its own range again. JoyDeadZone(joy) and JoySetDeadZone(joy, percent) are the dead zone round the middle (0 to 50 percent). JoyChoiceCount, JoyChoiceLabel$, JoyChoice, and JoySetChoice list and change the game port's mode (JOY_CHOICE_GAMEPORT) and when the keyboard joystick stands in (JOY_CHOICE_FALLBACK); JoyKeyCount and JoyKeyName$(index) list the keys the keyboard joystick can use, and JoyKbdSlot(slot) and JoySetKbdSlot(slot, keyIndex) its keys for up, down, left, right, and buttons 1 to 4 (slots 0 to 7). These lists are numbered from 0, so they fill a DropDown in order. JoyKeyIndex(keyName) gives a key's number by its name (-1 when unknown), JoyKeyCode(index) its raw key code, which GameKeyDown in game.bas takes, and JoyKeyNameOf$(rawCode) the name of a raw key code. JoyIsKeyboard(joy) is True for the keyboard standing in for a joystick, and JoyCalibration(joy, axis, part) reads back an axis's calibration: part JOY_CAL_MIN, JOY_CAL_CENTER, or JOY_CAL_MAX of the readings set with JoySetCalibration, or of the range the axis found by itself (0 for a joystick that cannot be calibrated). JoySetting$(key, default) and JoySetSetting key, value reach any other joystick setting; JOY_KEY_POLL_HZ is the key of how often joysticks are read, JOY_POLL_HZ_MIN to JOY_POLL_HZ_MAX times a second (JOY_POLL_HZ_DEFAULT normally), and dead zones go up to JOY_DEADZONE_MAX percent (JOY_DEADZONE_DEFAULT normally). After a new game port mode, JoyRescan() looks for joysticks again; it returns the new count, or -1 while another program is using a joystick. Every change applies at once; JoySettingsStore handle puts them all into a settings handle from prefs.bas for the program to save, and JoySettingsLoad handle takes them back from one, to undo.

    Sub cmdCenter_Click ()
         ' The stick rests in the middle: remember the readings
         centerX = JoyRawAxis(1, 1)
    @@ -5179,7 +5302,7 @@ Sub cmdFinish_Click ()
     End Sub

    See: STICK and STRIG

    Sound

    -

    With sound.bas in the project, a program plays sound files and tones through whatever sound hardware the computer has: a Sound Blaster, the PC speaker (tones only, unless sampled sound on the speaker is switched on) or nothing at all, in which case the program works the same but is silent. Several programs can play at once, and everything a program plays stops when it ends.

    +

    With sound.bas in the project, a program plays sound files and tones through whatever sound hardware the computer has: a Sound Blaster, the PC speaker (tones only, unless sampled sound on the speaker is switched on), or nothing at all, in which case the program works the same but is silent. Several programs can play at once, and everything a program plays stops when it ends.

    PlayWave(fileName) plays a WAV or VOC file once and PlayWaveLoop(fileName) over and over; PlayTone(frequency, milliseconds, volume) plays a tone of frequency hertz (20 to 20000) and PlayNoise(frequency, milliseconds, volume) noise, which sounds brighter the higher the frequency; a length of 0 plays until stopped. Each returns a voice number, or SOUND_FAILED (0) when the sound could not be played, and then SoundError$() says why. StopWave(voice) stops one sound, StopAllSounds() all of the program's sounds, and SoundPlaying(voice) is True while a sound is still playing. SoundVolume(volume) sets the volume of everything the program plays. Volumes are percent (0 to 100). SoundInfo$() describes the sound device, like "Sound Blaster 16 (DSP 4.05) at 220h, 22050 Hz, 16-bit stereo".

    Dim Shared boom As Long
     
    @@ -5190,9 +5313,9 @@ Sub cmdFire_Click ()
             lblStatus.Caption = SoundError$()
         End If
     End Sub
    -

    WAV files must be uncompressed (8 or 16 bits, mono or stereo); VOC files may be 8- or 16-bit. QBasic's BEEP, SOUND and PLAY work too, without sound.bas.

    -

    PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator) and "running" are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits" and "channels" are numbers. SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played.

    -

    A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. A program's sounds are freed when it ends.

    +

    WAV files must be uncompressed (8 or 16 bits, mono or stereo); VOC files may be 8- or 16-bit. QBasic's BEEP, SOUND, and PLAY work too, without sound.bas.

    +

    PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator), and "running" are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits", and "channels" are numbers. SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played.

    +

    A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan, and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. A program's sounds are freed when it ends.

    Dim Shared laser As Long
     
     Sub Form1_Load ()
    @@ -5204,15 +5327,15 @@ Sub Fire (x As Integer)
         v = SoundPlay(laser, 80, (x - 160) * 100 \ 160, False)
         SoundSetPitch v, 90 + Int(Rnd * 20)
     End Sub
    -

    A program can also make its sound as it goes, for speech or music of its own. SoundStreamOpen(rate, bits, channels, bufferMs) opens a stream that starts playing at once (silence until data arrives) and holds bufferMs milliseconds of sound (0 for the usual SOUND_STREAM_MS_DEFAULT; SOUND_STREAM_MS_MIN to SOUND_STREAM_MS_MAX). SoundStreamWrite(stream, data) adds raw sound data in the stream's format and returns how many bytes it took; keep the rest and write it again later. SoundStreamSpace(stream) is the room left and SoundStreamQueued(stream) what is waiting to play, both in samples (a stereo sample counts once). SoundStreamVoice(stream) is the stream's voice, for SoundSetVolume, SoundSetPan and SoundSetPitch, and SoundStreamClose stream stops it. A Timer keeps a stream fed:

    +

    A program can also make its sound as it goes, for speech or music of its own. SoundStreamOpen(rate, bits, channels, bufferMs) opens a stream that starts playing at once (silence until data arrives) and holds bufferMs milliseconds of sound (0 for the usual SOUND_STREAM_MS_DEFAULT; SOUND_STREAM_MS_MIN to SOUND_STREAM_MS_MAX). SoundStreamWrite(stream, data) adds raw sound data in the stream's format and returns how many bytes it took; keep the rest and write it again later. SoundStreamSpace(stream) is the room left and SoundStreamQueued(stream) what is waiting to play, both in samples (a stereo sample counts once). SoundStreamVoice(stream) is the stream's voice, for SoundSetVolume, SoundSetPan, and SoundSetPitch, and SoundStreamClose stream stops it. A Timer keeps a stream fed:

    Sub Timer1_Timer ()
         Dim taken As Long
         taken = SoundStreamWrite(stream, pending)
         pending = Mid$(pending, taken + 1)
         If Len(pending) = 0 Then pending = MakeMoreSound()
     End Sub
    -

    On a computer with an FM synthesizer chip (AdLib and most Sound Blasters), a program can play instruments of its own. FmInfo$(field) describes the chip: "description" (like "OPL3 FM synthesizer at 388h", "" when there is none), "voices", "free" (voices no program has), "stereo" and "owned" (one program has the whole chip). FmAlloc() takes one of its two-operator voices (0 when none is free), FmWrite(voice, register, value) sets one of the voice's registers (the FM_ constants: FM_MOD_CHAR, FM_CAR_LEVEL, FM_FNUM_LOW, FM_KEY_BLOCK with FM_KEY_ON, ...; True when it worked), and FmFree voice gives it back. A player of chip music files can take the whole chip with FmRawAcquire() (False while another program has it), set any register with FmRawWrite(register, value) (bank * FM_BANK_REGS + register; the second bank only on an OPL3) and give it back with FmRawRelease. A frequency number for FM_FNUM_LOW and FM_KEY_BLOCK is hertz * 2 ^ 20 / (FM_CHIP_HZ * 2 ^ block).

    -

    sound.bas also changes DVX's own sound settings, as the Control Panel's Sounds page does. DVX plays a sound for each of SoundEventCount() events, numbered from 0: SoundEventLabel$(eventIdx) names one ("Start DVX", "Error", ...) and SoundEventKey$(eventIdx) gives its key in DVX.INI. With a settings handle from prefs.bas (PrefsLoad(PrefsDvxIni$())), SoundEventFile$(handle, eventIdx) is the event's sound file ("" for none) and SoundSetEventFile handle, eventIdx, fileName changes it; after the program saves the file, SoundEventsReload makes DVX play the new sounds. SoundTestFile(fileName) plays a file the way DVX plays its event sounds (0 when it cannot, and SoundError$() says why), and SoundFolder$() is the folder of DVX's own sounds. SoundMasterVolume() and SoundSetMasterVolume percent are the volume of all sound from every program. SoundDriverCount() reads the list of sound drivers and SoundDriverName$(index) names each; SoundDriverAuto$() is the setting that means "find the best one". SoundMidiCount() and SoundMidiLabel$(index) list where MIDI music can play. SoundSettingDriver$(handle), SoundSettingMidi(handle) and SoundSettingVolume(handle) read the sound driver, MIDI choice and master volume from the settings, and SoundSettingsStore handle, driver, midi, volume stores them (a new driver or MIDI choice takes effect when DVX next starts).

    +

    On a computer with an FM synthesizer chip (AdLib and most Sound Blasters), a program can play instruments of its own. FmInfo$(field) describes the chip: "description" (like "OPL3 FM synthesizer at 388h", "" when there is none), "voices", "free" (voices no program has), "stereo", and "owned" (one program has the whole chip). FmAlloc() takes one of its two-operator voices (0 when none is free), FmWrite(voice, register, value) sets one of the voice's registers (the FM_ constants: FM_MOD_CHAR, FM_CAR_LEVEL, FM_FNUM_LOW, FM_KEY_BLOCK with FM_KEY_ON, ...; True when it worked), and FmFree voice gives it back. A player of chip music files can take the whole chip with FmRawAcquire() (False while another program has it), set any register with FmRawWrite(register, value) (bank * FM_BANK_REGS + register; the second bank only on an OPL3) and give it back with FmRawRelease. A frequency number for FM_FNUM_LOW and FM_KEY_BLOCK is hertz * 2 ^ 20 / (FM_CHIP_HZ * 2 ^ block).

    +

    sound.bas also changes DVX's own sound settings, as the Control Panel's Sounds page does. DVX plays a sound for each of SoundEventCount() events, numbered from 0: SoundEventLabel$(eventIdx) names one ("Start DVX", "Error", ...) and SoundEventKey$(eventIdx) gives its key in DVX.INI. With a settings handle from prefs.bas (PrefsLoad(PrefsDvxIni$())), SoundEventFile$(handle, eventIdx) is the event's sound file ("" for none) and SoundSetEventFile handle, eventIdx, fileName changes it; after the program saves the file, SoundEventsReload makes DVX play the new sounds. SoundTestFile(fileName) plays a file the way DVX plays its event sounds (0 when it cannot, and SoundError$() says why), and SoundFolder$() is the folder of DVX's own sounds. SoundMasterVolume() and SoundSetMasterVolume percent are the volume of all sound from every program. SoundDriverCount() reads the list of sound drivers and SoundDriverName$(index) names each; SoundDriverAuto$() is the setting that means "find the best one". SoundMidiCount() and SoundMidiLabel$(index) list where MIDI music can play. SoundSettingDriver$(handle), SoundSettingMidi(handle), and SoundSettingVolume(handle) read the sound driver, MIDI choice, and master volume from the settings, and SoundSettingsStore handle, driver, midi, volume stores them (a new driver or MIDI choice takes effect when DVX next starts).

    Dim ini As Long
     Dim i As Long
     
    @@ -5221,9 +5344,9 @@ ini = PrefsLoad(PrefsDvxIni$())
     For i = 0 To SoundEventCount() - 1
         lstEvents.AddItem SoundEventLabel$(i) & ": " & SoundEventFile$(ini, i)
     Next i
    -

    See: BEEP, SOUND and PLAY

    +

    See: BEEP, SOUND, and PLAY

    Music

    -

    With music.bas in the project, a program plays music files: tracker modules (MOD, S3M, XM and IT files, made of recorded instrument sounds) wherever the computer can play sampled sound, and MIDI files (.MID, types 0 and 1) on the computer's music synthesizer, or on a built-in synthesizer that sounds much the same when there is none. Several programs can play music at once, and everything a program plays stops when it ends.

    +

    With music.bas in the project, a program plays music files: tracker modules (MOD, S3M, XM, and IT files, made of recorded instrument sounds) wherever the computer can play sampled sound, and MIDI files (.MID, types 0 and 1) on the computer's music synthesizer, or on a built-in synthesizer that sounds much the same when there is none. Several programs can play music at once, and everything a program plays stops when it ends.

    MusicLoad(fileName) loads a music file and returns its music number, or MUSIC_FAILED (0) when it could not be loaded; MusicError$() then says why. MusicPlay(music, loopIt) plays it from where it is (the start after loading or stopping); with loopIt True it starts over at the end until it is stopped. MusicStop(music) stops and goes back to the start, MusicPause(music) and MusicResume(music) pause and go on, and MusicPlaying(music) is True while it plays. MusicVolume(music, volume) sets its volume in percent (0 to 100). MusicPosition(music) and MusicLength(music) give the place and the length in milliseconds, and MusicSeek(music, milliseconds) goes to a place (a module goes to the start of the part that holds it). MusicFree(music) stops a music and forgets it.

    Dim Shared tune As Long
     
    @@ -5236,7 +5359,7 @@ Sub Form1_Load ()
             MusicPlay tune, True
         End If
     End Sub
    -

    A program can also play notes itself, like a keyboard. MusicMidiOpen() returns a live music that is already playing. MusicNoteOn(music, channel, note, velocity) starts a note and MusicNoteOff(music, channel, note) ends it; MusicProgram(music, channel, program) chooses a channel's instrument, MusicControl(music, channel, controller, value) sets a controller (7 volume, 10 pan, 11 expression, 64 the sustain pedal, 123 all notes off) and MusicPitchBend(music, channel, bend) bends the channel's notes (-8192 to 8191, two semitones either way at the ends). Channels are 1 to 16, and channel 10 (MUSIC_DRUMS) plays drums, one per note. Notes, velocities, instruments and controller values are 0 to 127; middle C is note 60, and instruments are numbered as in General MIDI (0 piano, 24 guitar, 40 violin, 56 trumpet, 73 flute). The same calls work on a MIDI file that is playing.

    +

    A program can also play notes itself, like a keyboard. MusicMidiOpen() returns a live music that is already playing. MusicNoteOn(music, channel, note, velocity) starts a note and MusicNoteOff(music, channel, note) ends it; MusicProgram(music, channel, program) chooses a channel's instrument, MusicControl(music, channel, controller, value) sets a controller (7 volume, 10 pan, 11 expression, 64 the sustain pedal, 123 all notes off), and MusicPitchBend(music, channel, bend) bends the channel's notes (-8192 to 8191, two semitones either way at the ends). Channels are 1 to 16, and channel 10 (MUSIC_DRUMS) plays drums, one per note. Notes, velocities, instruments, and controller values are 0 to 127; middle C is note 60, and instruments are numbered as in General MIDI (0 piano, 24 guitar, 40 violin, 56 trumpet, 73 flute). The same calls work on a MIDI file that is playing.

    Dim Shared keys As Long
     
     Sub Form1_Load ()
    @@ -5253,8 +5376,8 @@ Sub tmrNoteOff_Timer ()
         MusicNoteOff keys, 1, 60
         tmrNoteOff.Enabled = False
     End Sub
    -

    MusicGetVolume(music) gives a music's volume in percent. MusicInfo$(music, field) tells about it, by name in any case: "title" (a module's own title), "output" (where it plays, for people), "kind" ("module", "midi" or "live"), "where" ("stream", "mpu401", "fm" or "software"; "" before it first plays), and "paused" and "looping" (True or False as text; VAL gives the value).

    -

    A program that sends MIDI itself (system exclusive messages, a sequencer of its own) can have the MIDI port, one program at a time, when the settings send MIDI music to the MIDI port. MidiPortInfo$(field) describes it: "description" ("" when there is none), "present" and "owned" (a program has it). MidiAcquire() takes the port (False when another program has it or there is none), MidiPut(byteValue) sends one byte (False when the port could not take it yet: send it again) and MidiRelease gives the port back.

    +

    MusicGetVolume(music) gives a music's volume in percent. MusicInfo$(music, field) tells about it, by name in any case: "title" (a module's own title), "output" (where it plays, for people), "kind" ("module", "midi", or "live"), "where" ("stream", "mpu401", "fm", or "software"; "" before it first plays), and "paused" and "looping" (True or False as text; VAL gives the value).

    +

    A program that sends MIDI itself (system exclusive messages, a sequencer of its own) can have the MIDI port, one program at a time, when the settings send MIDI music to the MIDI port. MidiPortInfo$(field) describes it: "description" ("" when there is none), "present", and "owned" (a program has it). MidiAcquire() takes the port (False when another program has it or there is none), MidiPut(byteValue) sends one byte (False when the port could not take it yet: send it again), and MidiRelease gives the port back.

    Network Connections

    With net.bas in the project, a program talks to other computers over the network set up in the Control Panel. TcpConnect returns a handle (0 when it failed; NetError$ says why), TcpSend sends text, and TcpRecv$ returns whatever has arrived without waiting, so a Timer keeps the program responsive:

    Dim conn As Long
    @@ -5344,7 +5467,7 @@ If msg <> "" And Not CommRecvEncrypted(link, 1) Then
         Print "Unencrypted: " & msg
     End If

    Connections and Terminals

    -

    With conn.bas in the project, a program talks to another computer the way the Terminal program does, whatever is in between: a serial port, a modem (it sends its setup command, dials, dials again when the line is busy and notices when the carrier is lost), the secure link to the DVX host proxy (encrypted, and authenticated with a passphrase), or Telnet over the network. ConnCreate makes a connection (a number); ConnSetSerial, ConnSetModem, ConnSetHost and ConnSetPassphrase set it up, and ConnOpen(conn, type) starts it. Opening only starts: dialing and the key exchange go on while the program runs, so call ConnPoll often from a Timer. ConnState says where it is (CONN_CLOSED, CONN_OPENING, CONN_OPEN) and ConnStatus$ what is happening or why it ended; ConnStatusSerial changes whenever ConnStatus$ does.

    +

    With conn.bas in the project, a program talks to another computer the way the Terminal program does, whatever is in between: a serial port, a modem (it sends its setup command, dials, dials again when the line is busy, and notices when the carrier is lost), the secure link to the DVX host proxy (encrypted, and authenticated with a passphrase), or Telnet over the network. ConnCreate makes a connection (a number); ConnSetSerial, ConnSetModem, ConnSetHost, and ConnSetPassphrase set it up, and ConnOpen(conn, type) starts it. Opening only starts: dialing and the key exchange go on while the program runs, so call ConnPoll often from a Timer. ConnState says where it is (CONN_CLOSED, CONN_OPENING, CONN_OPEN) and ConnStatus$ what is happening or why it ended; ConnStatusSerial changes whenever ConnStatus$ does.

    ConnAttach conn, "Term1" puts what arrives on the Terminal control Term1 (ConnPoll moves it there) and sends what is typed in it. ConnPoll then also appends what arrives to a capture file (ConnCapture), answers the other side's ENQ with an answerback (ConnSetAnswerback), and notices a ZMODEM sender starting a download: ConnZmodemRequest is True once, and the program answers with ConnReceiveFiles in the same Timer event. ConnSetEcho shows what is typed on the Terminal too. Without a Terminal, read with ConnRead$ and send with ConnWrite (any bytes, CHR$(0) too). ConnOpen returns CONN_OK or a CONN_ERR_ code; ConnErrorText$ puts the code in words. ConnSetXferWindow conn, bytes limits how much a ZMODEM sender may send before it waits for an answer (0 = no limit, up to XFER_WINDOW_MAX; -1, the default, picks by the line). ConnSendBreak holds a serial line in the break state for a moment; ConnClose hangs up and ConnFree forgets the connection.

    Dim Shared conn As Long
     Dim Shared download As Long
    @@ -5361,7 +5484,7 @@ Sub Timer1_Timer ()
         If ConnZmodemRequest(conn) Then download = ConnReceiveFiles(conn, XFER_ZMODEM, "DOWNLOAD", XFER_EXIST_RENAME, False)
     End Sub

    File Transfers

    -

    With xfer.bas in the project, a program sends and receives files with XMODEM, XMODEM-1K, YMODEM or ZMODEM. A transfer is a number: XferSendFile and XferReceive start one on a serial port opened with SerOpen (comm.bas), and ConnSendFiles and ConnReceiveFiles start one on a connection (conn.bas), with the options Terminal has: escaping every control character, asking the receiver to continue interrupted files, and what to do with a file that is already there. On a serial port XferPoll moves the transfer along; on a connection ConnPoll does. XferStatus tells where it is (XFER_RUNNING until it ends with XFER_DONE, XFER_FAILED or XFER_CANCELLED), and XferBytes, XferSize, XferFile$, XferFileIndex, XferFileCount, XferBatchBytes, XferBatchSize, XferResumedAt and XferErrors show its progress; XferMessage$ says how it ended. A receive with XFER_EXIST_ASK stops in XFER_ASKING when a file already exists: XferAskSize, XferAskRemoteSize (the size the sender offers, -1 when it did not say) and XferCanResume describe it, and XferAnswer continues with an XFER_EXIST_ choice. XferCancel stops a transfer and XferClose frees it.

    +

    With xfer.bas in the project, a program sends and receives files with XMODEM, XMODEM-1K, YMODEM, or ZMODEM. A transfer is a number: XferSendFile and XferReceive start one on a serial port opened with SerOpen (comm.bas), and ConnSendFiles and ConnReceiveFiles start one on a connection (conn.bas), with the options Terminal has: escaping every control character, asking the receiver to continue interrupted files, and what to do with a file that is already there. On a serial port XferPoll moves the transfer along; on a connection ConnPoll does. XferStatus tells where it is (XFER_RUNNING until it ends with XFER_DONE, XFER_FAILED, or XFER_CANCELLED), and XferBytes, XferSize, XferFile$, XferFileIndex, XferFileCount, XferBatchBytes, XferBatchSize, XferResumedAt, and XferErrors show its progress; XferMessage$ says how it ended. A receive with XFER_EXIST_ASK stops in XFER_ASKING when a file already exists: XferAskSize, XferAskRemoteSize (the size the sender offers, -1 when it did not say), and XferCanResume describe it, and XferAnswer continues with an XFER_EXIST_ choice. XferCancel stops a transfer and XferClose frees it.

    XferCrc16(bytes) and XferCrc32(bytes) give the check values the protocols use (CRC-16 as XMODEM computes it, 0 to 65535; the CRC-32 of ZMODEM and ZIP files as a Long, negative when its top bit is set) for any bytes. XferSafeName$(name, folder) gives the name a receive into folder stores a sender's file name as: no folder or device name; a long name, its case kept, where folder's disk takes long names (a character a name cannot hold becomes _), else an 8.3 name of characters every disk accepts.

    Lists You Draw Yourself

    With listhelp.bas in the project, a list or icon view a program draws itself -- in a UserControl or a PictureBox -- follows the same rules as ListBox and ListView. ListSelCreate(multi, count) makes a selection for count items (several at a time when multi is True) and returns a handle; it remembers which items are selected and which one is current (the one the keyboard moves). Items are numbered from 0. Give it the clicks and keys, and ask it what to draw:

    @@ -5369,7 +5492,7 @@ End Sub -------- ----------- ListSelClick sel, index, shift A click on an item, with the MouseDown event's Shift: alone it selects only that item, with Ctrl it turns the item on or off, with Shift it selects the items from the last click up to it. ListSelKey(sel, keyCode, shift) Ctrl+A selects every item (returns LISTSEL_KEY_ALL), Space turns the current item on or off (LISTSEL_KEY_TOGGLE); LISTSEL_KEY_NONE for other keys. - ListSelNavigate(sel, keyCode, shift, columns, pageRows) The arrow keys, Home, End, PgUp and PgDn, in a list (columns 1) or a grid with columns items per row, pageRows rows a page; with Shift they select from the last click. The new current item, or -1 when nothing moved. + ListSelNavigate(sel, keyCode, shift, columns, pageRows) The arrow keys, Home, End, PgUp, and PgDn, in a list (columns 1) or a grid with columns items per row, pageRows rows a page; with Shift they select from the last click. The new current item, or -1 when nothing moved. ListSelMove sel, index, shift Make an item current as the keyboard does (after type-ahead). ListSelCursor(sel), ListSelSetCursor sel, index The current item (-1: none); make one item current and the only one selected. ListSelIsSelected(sel, index), ListSelSelectedCount(sel) Whether an item is selected; how many are. @@ -5395,13 +5518,13 @@ Sub UserControl_KeyPress (KeyAscii As Integer) End If End Sub

    Selections a program leaves open are freed when it ends.

    -

    An icon view drawn with listhelp.bas looks and behaves like Program Manager's: the same cells for large icons, small icons and the list, the same frame and scroll bar, a large icon's name on two lines, and the same rules for the keys, dragging and dropping. The grid functions take the view mode (LISTICON_VIEW_LARGE, LISTICON_VIEW_SMALL or LISTICON_VIEW_LIST), the size of a character (TextWidth("W") and TextHeight("W")), the size of the whole view, how many items it shows, and rowsTaken: LISTICON_BY_ORDER when the items fill the grid in order, else how many rows items placed in cells of their own take. Draw the view's frame with DrawBevel (LISTICON_FRAME wide; with the face -1 after the items, so a row cut off at the bottom stops at the frame) and its scroll bar with DrawScrollBar as a control's bar (its control argument True); the grid leaves room for it.

    +

    An icon view drawn with listhelp.bas looks and behaves like Program Manager's: the same cells for large icons, small icons, and the list, the same frame and scroll bar, a large icon's name on two lines, and the same rules for the keys, dragging and dropping. The grid functions take the view mode (LISTICON_VIEW_LARGE, LISTICON_VIEW_SMALL, or LISTICON_VIEW_LIST), the size of a character (TextWidth("W") and TextHeight("W")), the size of the whole view, how many items it shows, and rowsTaken: LISTICON_BY_ORDER when the items fill the grid in order, else how many rows items placed in cells of their own take. Draw the view's frame with DrawBevel (LISTICON_FRAME wide; with the face -1 after the items, so a row cut off at the bottom stops at the frame) and its scroll bar with DrawScrollBar as a control's bar (its control argument True); the grid leaves room for it.

      Function                                            Description
       --------                                            -----------
    -  ListIconMetric(mode, metric, cw, ch, w, h, count, rowsTaken)  One value of the grid: a cell's size (LISTICON_CELL_W, LISTICON_CELL_H), where its picture goes and how big it is (LISTICON_PICTURE_X, LISTICON_PICTURE_Y, LISTICON_PICTURE_SIZE), where its name goes and how many characters it shows (LISTICON_TEXT_X, LISTICON_TEXT_Y, LISTICON_NAME_CHARS; a large icon's two lines are each centred in the cell), the list's detail column (LISTICON_DETAIL_X, 0 in the other views); the inside of the frame less the scroll bar (LISTICON_INNER_X, LISTICON_INNER_Y, LISTICON_INNER_W, LISTICON_INNER_H), the columns, the rows shown whole and in all (LISTICON_COLUMNS, LISTICON_ROWS_SHOWN, LISTICON_ROWS), the largest top row (LISTICON_TOP_ROW_MAX), True when the scroll bar is needed (LISTICON_SCROLLBAR), where the text of an empty view goes (LISTICON_EMPTY_X, LISTICON_EMPTY_Y), the frame's width (LISTICON_FRAME) and the rows a wheel notch scrolls (LISTICON_WHEEL_ROWS).
    +  ListIconMetric(mode, metric, cw, ch, w, h, count, rowsTaken)  One value of the grid: a cell's size (LISTICON_CELL_W, LISTICON_CELL_H), where its picture goes and how big it is (LISTICON_PICTURE_X, LISTICON_PICTURE_Y, LISTICON_PICTURE_SIZE), where its name goes and how many characters it shows (LISTICON_TEXT_X, LISTICON_TEXT_Y, LISTICON_NAME_CHARS; a large icon's two lines are each centred in the cell), the list's detail column (LISTICON_DETAIL_X, 0 in the other views); the inside of the frame less the scroll bar (LISTICON_INNER_X, LISTICON_INNER_Y, LISTICON_INNER_W, LISTICON_INNER_H), the columns, the rows shown whole and in all (LISTICON_COLUMNS, LISTICON_ROWS_SHOWN, LISTICON_ROWS), the largest top row (LISTICON_TOP_ROW_MAX), True when the scroll bar is needed (LISTICON_SCROLLBAR), where the text of an empty view goes (LISTICON_EMPTY_X, LISTICON_EMPTY_Y), the frame's width (LISTICON_FRAME), and the rows a wheel notch scrolls (LISTICON_WHEEL_ROWS).
       ListIconCellAt(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y)  The cell under x, y with the view scrolled to topRow, numbered row * columns + column; -1 outside the grid.
       ListIconLabel$(name, maxChars, lineNo)              Line 1 or 2 of a large icon's name, maxChars characters a line (LISTICON_NAME_CHARS): the first line ends at the last blank that fits, else it is cut; the second is the rest, cut. "" for a line the name does not need.
    -  ListIconNavigate(keyCode, current, cells, columns, pageRows)  The item the arrow keys, Home, End, PgUp or PgDn move to when the items stand in cells of their own, gaps allowed: cells holds each item's column and row, all separated by commas ("0,0,2,0,1,1"). -1 for another key or when no item is that way.
    +  ListIconNavigate(keyCode, current, cells, columns, pageRows)  The item the arrow keys, Home, End, PgUp, or PgDn move to when the items stand in cells of their own, gaps allowed: cells holds each item's column and row, all separated by commas ("0,0,2,0,1,1"). -1 for another key or when no item is that way.
       ListIconScrollPress(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y)  A left press on the scroll bar: the top row after it (an arrow moves a row, the trough a page, the thumb nothing); -1 when x, y is not on the scroll bar.
       ListIconScrollGrab(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y)  How far below the top of the thumb a press is; -1 when it is not on the thumb.
       ListIconScrollDrag(mode, cw, ch, w, h, count, rowsTaken, grab, y)  The top row with the thumb, held grab pixels below its top, dragged to y. While the button is down the control gets the mouse even outside it.
    @@ -5429,7 +5552,7 @@ End Sub
    SecCipherCreate(key) A cipher with a SEC_XTEA_KEY_SIZE-byte key; 0 for a key of another length. SecCipherCrypt$(cipher, bytes) Encrypt -- or decrypt, the same call -- bytes. SecCipherSetCounter cipher, n sets the position in the key stream; the same key and position must never encrypt two different messages. SecCipherFree cipher frees it. SecSha256$(bytes) The SHA-256 hash (SEC_SHA256_SIZE bytes). For data in pieces: SecSha256Open, SecSha256Add h, piece, then SecSha256End$(h), which also frees the handle. - SecHmac$(key, bytes) The HMAC-SHA256 signature of bytes with key; SecHmacOpen(key), SecHmacAdd and SecHmacEnd$ for pieces. + SecHmac$(key, bytes) The HMAC-SHA256 signature of bytes with key; SecHmacOpen(key), SecHmacAdd, and SecHmacEnd$ for pieces. SecHkdf$(salt, secret, info, length) length bytes (up to SEC_HKDF_MAX_OUT) of keys made from secret; a different info gives different keys. SecEqual(a, b) True when a and b hold the same bytes, compared in the same time wherever they differ (for signature checks).
    ' Both sides: make keys, swap public keys (here over a TCP connection), agree a cipher key
    @@ -5444,23 +5567,23 @@ End If
     SecDhFree dh

    Handles a program leaves open are freed, and their keys wiped, when it ends.

    Settings Files

    -

    With prefs.bas in the project, a program keeps its settings in an INI file the way DVX's own programs do. PrefsLoad(path) reads the file once (a file that is not there reads as empty); PrefsGet$, PrefsGetInt and PrefsGetBool read a key of a section (each with the value to use when it is missing), and PrefsSet, PrefsSetInt, PrefsSetBool and PrefsRemove change it in memory. PrefsSave writes it back, keeping comments and every line it did not change. To write a whole new file -- dropping sections that are no longer wanted -- fill a PrefsCreate handle and save it with PrefsSaveAs: the old file is replaced only once the new one is completely written. PrefsClose frees a handle. IniRead and IniWrite (built in) do one value at a time and read or write the whole file each time; prefs.bas is the faster choice for many settings.

    +

    With prefs.bas in the project, a program keeps its settings in an INI file the way DVX's own programs do. PrefsLoad(path) reads the file once (a file that is not there reads as empty); PrefsGet$, PrefsGetInt, and PrefsGetBool read a key of a section (each with the value to use when it is missing), and PrefsSet, PrefsSetInt, PrefsSetBool, and PrefsRemove change it in memory. PrefsSave writes it back, keeping comments and every line it did not change. To write a whole new file -- dropping sections that are no longer wanted -- fill a PrefsCreate handle and save it with PrefsSaveAs: the old file is replaced only once the new one is completely written. PrefsClose frees a handle. IniRead and IniWrite (built in) do one value at a time and read or write the whole file each time; prefs.bas is the faster choice for many settings.

    Dim ini As Long
     ini = PrefsLoad(ShellConfigFile$("MYAPP.INI"))
     userName = PrefsGet$(ini, "User", "Name", "Unknown")
     PrefsSetInt ini, "Window", "Left", Me.Left
     If Not PrefsSave(ini) Then MsgBox "The settings could not be saved."
     PrefsClose ini
    -

    PrefsDvxIni$() is the path of DVX's own settings file, DVX.INI, which Control Panel changes; other libraries (system.bas, joystick.bas, net.bas, print.bas and sound.bas) read and store their part of it through a prefs.bas handle. PrefsSetVideoMode handle, wide, high, bpp stores the screen mode DVX starts in, and PrefsVideoWidth(handle), PrefsVideoHeight(handle) and PrefsVideoBpp(handle) read it (DVX's own start-up mode, SYS_VIDEO_DEFAULT_* in system.bas, for a value that is not there). PrefsKeyName$(handle, section, index) is the name of key number index of a section (from 0, in the order of the file; "" past the last key), to go through every key of a section.

    -

    Some settings are stored by name, from a fixed list of choices: the mouse acceleration (PREFS_CHOICE_ACCEL), the wallpaper mode (PREFS_CHOICE_WALLPAPER) and the scroll wheel direction (PREFS_CHOICE_WHEEL). For each list, PrefsChoiceCount(table) says how many choices it has (numbered from 0), PrefsChoiceLabel$(table, i) is the text to show, PrefsChoiceName$(table, i) the name stored in the file and PrefsChoiceValue(table, i) the value it stands for (what SysSetMouse takes). PrefsChoiceIndex(table, choiceName) and PrefsChoiceIndexOfValue(table, value) find a choice (-1 when there is none), and PrefsGetChoice(handle, section, key, table) reads one from a file, giving DVX's default choice when the key is missing or unknown. PrefsChoiceDefault(table) is that default choice, what a Defaults button selects. For a list of your own, PrefsGetChoiceFrom(handle, section, key, choiceNames, defaultName) does the same with the names separated by "|" ("large|small|list" stores choice 1 as "small"), giving defaultName's choice for a missing or unknown key.

    +

    PrefsDvxIni$() is the path of DVX's own settings file, DVX.INI, which Control Panel changes; other libraries (system.bas, joystick.bas, net.bas, print.bas, and sound.bas) read and store their part of it through a prefs.bas handle. PrefsSetVideoMode handle, wide, high, bpp stores the screen mode DVX starts in, and PrefsVideoWidth(handle), PrefsVideoHeight(handle), and PrefsVideoBpp(handle) read it (DVX's own start-up mode, SYS_VIDEO_DEFAULT_* in system.bas, for a value that is not there). PrefsKeyName$(handle, section, index) is the name of key number index of a section (from 0, in the order of the file; "" past the last key), to go through every key of a section.

    +

    Some settings are stored by name, from a fixed list of choices: the mouse acceleration (PREFS_CHOICE_ACCEL), the wallpaper mode (PREFS_CHOICE_WALLPAPER), and the scroll wheel direction (PREFS_CHOICE_WHEEL). For each list, PrefsChoiceCount(table) says how many choices it has (numbered from 0), PrefsChoiceLabel$(table, i) is the text to show, PrefsChoiceName$(table, i) the name stored in the file, and PrefsChoiceValue(table, i) the value it stands for (what SysSetMouse takes). PrefsChoiceIndex(table, choiceName) and PrefsChoiceIndexOfValue(table, value) find a choice (-1 when there is none), and PrefsGetChoice(handle, section, key, table) reads one from a file, giving DVX's default choice when the key is missing or unknown. PrefsChoiceDefault(table) is that default choice, what a Defaults button selects. For a list of your own, PrefsGetChoiceFrom(handle, section, key, choiceNames, defaultName) does the same with the names separated by "|" ("large|small|list" stores choice 1 as "small"), giving defaultName's choice for a missing or unknown key.

    For i = 0 To PrefsChoiceCount(PREFS_CHOICE_ACCEL) - 1
         cboAccel.AddItem PrefsChoiceLabel$(PREFS_CHOICE_ACCEL, i)
     Next i
     cboAccel.ListIndex = PrefsChoiceIndexOfValue(PREFS_CHOICE_ACCEL, SysMouseAccel())

    Help Files

    With help.bas in the project, HelpCompile(source, output) compiles a help source file (.dhs) into a help file (.hlp), True when it worked; HelpCompileMessages$() gives the compiler's errors and warnings, one per line, each with the file and line ("error: NOTES.DHS:1: directive .h1 outside of a topic"). HelpView file opens a help file in the DVX Help Viewer (a file named without a folder is looked for beside the program; "" opens the DVX user guide). HelpViewTopic file, topicId opens it at a topic, the way F1 does. A Help Viewer that is already open shows the file in its window instead of a second copy starting.

    -

    With hlpfile.bas, a program reads help files itself -- everything a help viewer needs. HlpOpen(path) opens a help file and returns a handle (0 when it is not one); HlpClose closes it (a program's open files are closed when it ends). Topics are numbered from 0: HlpTopicCount, HlpTopicTitle$ and HlpTopicId$ (the name links and HelpTopic use), HlpFindTopic(handle, topicId) (-1 when there is none) and HlpStartTopic(handle, topicId), the topic to show first (topicId when the file has it, else the file's own start page). HlpTocCount, HlpTocTitle$, HlpTocDepth, HlpTocTopic and HlpTocExpanded give the table of contents in order -- a depth of 0 is the top level, a topic of -1 a heading that only groups others -- ready to fill a TreeView. HlpIndexCount, HlpIndexKeyword$ and HlpIndexTopic give the keyword index (sorted), for a list or DlgChoice. HlpSearch(handle, words) searches the titles, text and keywords as the Help Viewer does and returns how many topics have every word; HlpSearchTopic(handle, n) gives them.

    -

    To show a topic, put a HelpPane control on the form and call HelpPane1.ShowTopic handle, topic: headings, paragraphs, lists, notes, code, tables, pictures and links look as they do in the Help Viewer, and the text wraps to the control's width. A click on a link, or Enter on one reached with Tab, fires the HelpPane's LinkClick event with the ID of the topic the link names; the program decides what to do, usually show that topic and remember it for a Back button. A program that draws topics itself reads them with HlpLoadTopic(handle, topic) and HlpRecordType, HlpRecordFlags, HlpRecordText$ and HlpRecordLink$ (the HLP_REC_ constants), saves a picture for LOADPICTURE with HlpRecordImageSave, and wraps text with HlpWrap$.

    +

    With hlpfile.bas, a program reads help files itself -- everything a help viewer needs. HlpOpen(path) opens a help file and returns a handle (0 when it is not one); HlpClose closes it (a program's open files are closed when it ends). Topics are numbered from 0: HlpTopicCount, HlpTopicTitle$ and HlpTopicId$ (the name links and HelpTopic use), HlpFindTopic(handle, topicId) (-1 when there is none), and HlpStartTopic(handle, topicId), the topic to show first (topicId when the file has it, else the file's own start page). HlpTocCount, HlpTocTitle$, HlpTocDepth, HlpTocTopic, and HlpTocExpanded give the table of contents in order -- a depth of 0 is the top level, a topic of -1 a heading that only groups others -- ready to fill a TreeView. HlpIndexCount, HlpIndexKeyword$, and HlpIndexTopic give the keyword index (sorted), for a list or DlgChoice. HlpSearch(handle, words) searches the titles, text, and keywords as the Help Viewer does and returns how many topics have every word; HlpSearchTopic(handle, n) gives them.

    +

    To show a topic, put a HelpPane control on the form and call HelpPane1.ShowTopic handle, topic: headings, paragraphs, lists, notes, code, tables, pictures, and links look as they do in the Help Viewer, and the text wraps to the control's width. A click on a link, or Enter on one reached with Tab, fires the HelpPane's LinkClick event with the ID of the topic the link names; the program decides what to do, usually show that topic and remember it for a Back button. A program that draws topics itself reads them with HlpLoadTopic(handle, topic) and HlpRecordType, HlpRecordFlags, HlpRecordText$, and HlpRecordLink$ (the HLP_REC_ constants), saves a picture for LOADPICTURE with HlpRecordImageSave, and wraps text with HlpWrap$.

    Dim hlp As Long
     Dim history(63) As Long, histPos As Long
     
    @@ -5499,11 +5622,11 @@ End Sub
    lstFiles.MaxHeight = SizePercent(50)

    Compiling and Debugging Programs

    With compiler.bas in the project, a program compiles BASIC programs -- what a development tool written in BASIC needs. A source set holds the files of one program: CompSourceFromProject(src, dbpPath) reads a whole project (.dbp) from disk, or CompSourceAdd(src, path, text, kind) adds files one at a time, in the order a project compiles: modules, then class modules, then UserControls, then forms (COMP_KIND_AUTO tells them apart by the file name; a form's whole .frm text is added, and the code after its layout is compiled; a UserControl's whole .ctl text, its header and its code). CompCompile(src, optionExplicit, validate) gives a compiled module, or 0 with CompError$() saying what is wrong and CompErrorFile$() / CompErrorFileLine() where. With validate True, Label1.Captoin and other misspelled control members, and controls of an unknown type, are found while compiling instead of when the line runs (the set's own UserControls are known types).

    -

    Line numbers of compiled programs (errors, breakpoints, the line a program is paused on) count through all the files of the source set. CompMapFile$(src, line) and CompMapLine(src, line) give the file and the line in it; CompConcatLine(src, path, fileLine) goes the other way. CompSourceFiles$ lists the files in compile order. CompBuildApp(module, src, outPath, release) builds an .app the way the IDE's Make Executable does, with the project's name, icon and help file, its forms and its UserControls. For a control project (its Type setting is "Control") it builds a control file instead (outPath NAME.wgt), the way the IDE's Make Control does: the project's UserControls with their toolbox pictures and names, there for every program once the file is copied into DVX's WIDGETS folder (DVX loads it when it starts). CompProcList$(text) lists the SUB and FUNCTION blocks of source text with their first and last lines (also for code that does not compile yet), CompIsIdent checks a name and CompKeyword$ lists the language's keywords, CompKeywordClass the kind of each (COMP_KW_OTHER, COMP_KW_TYPE or COMP_KW_LITERAL) and CompKeywordReserved whether a program may also use it as a name of its own.

    -

    Project files: ProjOpen(dbpPath) opens a project file (.dbp) and ProjNew(name, folder) starts a new one; ProjSave writes it and ProjClose frees it. ProjFileCount, ProjFile$ (the name as the project lists it), ProjFilePath$ (the full path), ProjFileKind (COMP_KIND_MODULE, COMP_KIND_FORM, COMP_KIND_CLASS or COMP_KIND_CONTROL) and ProjFileFind list its files; ProjAddFile, ProjRemoveFile and ProjRenameFile (on disk and in the project together, saved at once) change the list. ProjGet$ and ProjSet read and set its settings by their names in the file: Name, Type ("Control" for a control project, empty for a program), Author, Publisher, Version, Copyright, Description, Icon, HelpFile, CommandArgs, StartupForm, OptionExplicit and MultiInstance (the last two "true" or "false"). This is the project model the DVX BASIC IDE uses.

    -

    A UserControl's header (the Begin UserControl block at the top of its .ctl file) is read and changed the way the IDE's Properties window does it, the rest of the text left as it is: ProjCtlKeys$() lists the keys in the window's order with the kind of value each takes (Name, Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, DefaultEvent; the kinds Name, Integer, Boolean, Color and Text), ProjCtlHeader$(text, key) reads one (its default when the header leaves it out), ProjCtlSetHeader$(text, key, value) gives the text with one set (an empty value removes it; Name renames the control) and ProjCtlNew$(name) the text of a new UserControl, as File > New UserControl makes it.

    -

    With debug.bas too, a program runs a compiled module as a child program inside itself: RunCreate(module, src, appDir, args) prepares it, RunStart(run, startupForm, breakOnStart) loads its forms, and RunPoll(run, ms), called from a Timer, runs it for at most ms milliseconds at a time and returns what it is doing: RUN_RUNNING, RUN_EVENTS (waiting for events on its forms), RUN_PAUSED, RUN_ENDED or RUN_ERROR (RunError$ and RunErrorLine tell why and where). RunOutput$ collects what it PRINTs; INPUT asks with an input box. RunStop ends it and RunFree forgets it.

    -

    DbgSetBreakpoints run, "12, 40" sets breakpoints (line numbers of the source set). While the program is paused, DbgLine and DbgProc$ say where, DbgLocals$ and DbgCallStack$ give the variables and the calls as rows for a ListView's SetRows, DbgEval$(run, expr) shows a variable or expression as the IDE's Watch window does, DbgExec$(run, line) runs an Immediate-window line (x = 5 changes a variable, anything else is printed) and DbgSetVar changes a variable; DbgStepInto, DbgStepOver, DbgStepOut, DbgRunToLine and DbgContinue go on, DbgPause stops it at the next line. The program's form captions show [break] while it is paused.

    +

    Line numbers of compiled programs (errors, breakpoints, the line a program is paused on) count through all the files of the source set. CompMapFile$(src, line) and CompMapLine(src, line) give the file and the line in it; CompConcatLine(src, path, fileLine) goes the other way. CompSourceFiles$ lists the files in compile order. CompBuildApp(module, src, outPath, release) builds an .app the way the IDE's Make Executable does, with the project's name, icon, and help file, its forms, and its UserControls. For a control project (its Type setting is "Control") it builds a control file instead (outPath NAME.wgt), the way the IDE's Make Control does: the project's UserControls with their toolbox pictures and names, there for every program once the file is copied into DVX's WIDGETS folder (DVX loads it when it starts). CompProcList$(text) lists the SUB and FUNCTION blocks of source text with their first and last lines (also for code that does not compile yet), CompIsIdent checks a name and CompKeyword$ lists the language's keywords, CompKeywordClass the kind of each (COMP_KW_OTHER, COMP_KW_TYPE, or COMP_KW_LITERAL), and CompKeywordReserved whether a program may also use it as a name of its own.

    +

    Project files: ProjOpen(dbpPath) opens a project file (.dbp) and ProjNew(name, folder) starts a new one; ProjSave writes it and ProjClose frees it. ProjFileCount, ProjFile$ (the name as the project lists it), ProjFilePath$ (the full path), ProjFileKind (COMP_KIND_MODULE, COMP_KIND_FORM, COMP_KIND_CLASS, or COMP_KIND_CONTROL), and ProjFileFind list its files; ProjAddFile, ProjRemoveFile, and ProjRenameFile (on disk and in the project together, saved at once) change the list. ProjGet$ and ProjSet read and set its settings by their names in the file: Name, Type ("Control" for a control project, empty for a program), Author, Publisher, Version, Copyright, Description, Icon, HelpFile, CommandArgs, StartupForm, OptionExplicit, and MultiInstance (the last two "true" or "false"). This is the project model the DVX BASIC IDE uses.

    +

    A UserControl's header (the Begin UserControl block at the top of its .ctl file) is read and changed the way the IDE's Properties window does it, the rest of the text left as it is: ProjCtlKeys$() lists the keys in the window's order with the kind of value each takes (Name, Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, DefaultEvent; the kinds Name, Integer, Boolean, Color, and Text), ProjCtlHeader$(text, key) reads one (its default when the header leaves it out), ProjCtlSetHeader$(text, key, value) gives the text with one set (an empty value removes it; Name renames the control), and ProjCtlNew$(name) the text of a new UserControl, as File > New UserControl makes it.

    +

    With debug.bas too, a program runs a compiled module as a child program inside itself: RunCreate(module, src, appDir, args) prepares it, RunStart(run, startupForm, breakOnStart) loads its forms, and RunPoll(run, ms), called from a Timer, runs it for at most ms milliseconds at a time and returns what it is doing: RUN_RUNNING, RUN_EVENTS (waiting for events on its forms), RUN_PAUSED, RUN_ENDED, or RUN_ERROR (RunError$ and RunErrorLine tell why and where). RunOutput$ collects what it PRINTs; INPUT asks with an input box. RunStop ends it and RunFree forgets it.

    +

    DbgSetBreakpoints run, "12, 40" sets breakpoints (line numbers of the source set). While the program is paused, DbgLine and DbgProc$ say where, DbgLocals$ and DbgCallStack$ give the variables and the calls as rows for a ListView's SetRows, DbgEval$(run, expr) shows a variable or expression as the IDE's Watch window does, DbgExec$(run, line) runs an Immediate-window line (x = 5 changes a variable, anything else is printed), and DbgSetVar changes a variable; DbgStepInto, DbgStepOver, DbgStepOut, DbgRunToLine, and DbgContinue go on, DbgPause stops it at the next line. The program's form captions show [break] while it is paused.

    A program paused in its module-level code waits until you step or continue it. One paused inside one of its event handlers -- or in its forms' Load code, which runs inside RunStart -- waits right there while your Timer and menus keep working; RunPoll called meanwhile returns at once, and RunStart returns only after such a pause.

    ' compiler.bas and debug.bas in the project; tmrRun is a Timer
     Dim Shared src As Long, mdl As Long, kid As Long
    @@ -5529,9 +5652,9 @@ Sub tmrRun_Timer
         End Select
     End Sub

    Form Designers and Code Editors

    -

    A form designer or a code editor written in BASIC uses the same pieces as the DVX BASIC IDE. designer.bas describes the control types, which CtlTypeName$(0 to CtlTypeCount - 1) gives in alphabetical order: the toolbox lists the types CtlTypeFlags marks CTL_FLAG_DESIGNABLE (with CtlTypeTooltip$ and the icon CtlTypeIconFile writes), new controls are named CtlTypeNamePrefix$ plus a number, and the properties window lists CtlPropName$ for 0 to CtlPropCount - 1 with CtlPropType (CTL_PROP_STRING ... CTL_PROP_ENUM, plus CTL_PROP_READONLY) and CtlPropEnum$ for an enumerated property's choices. "Form" and "Menu" stand for the form itself and a menu item. The event list is CtlEventList$, and CtlEventParams$ writes an event procedure's parameter list; CtlTypeEventParams$(typeName, eventName) does it for one type, whose events may have parameters of their own (a UserControl's events), and "UserControl" stands for the object in a UserControl's own code (UserControl_MouseDown has Button, Shift, X and Y). For a project's files, FrmFormName$ and FrmControlNames$ read a .frm file's form and control names (the Object list), FrmCode$ its code, and ClsClassName$ a class module's name.

    +

    A form designer or a code editor written in BASIC uses the same pieces as the DVX BASIC IDE. designer.bas describes the control types, which CtlTypeName$(0 to CtlTypeCount - 1) gives in alphabetical order: the toolbox lists the types CtlTypeFlags marks CTL_FLAG_DESIGNABLE (with CtlTypeTooltip$ and the icon CtlTypeIconFile writes), new controls are named CtlTypeNamePrefix$ plus a number, and the properties window lists CtlPropName$ for 0 to CtlPropCount - 1 with CtlPropType (CTL_PROP_STRING ... CTL_PROP_ENUM, plus CTL_PROP_READONLY) and CtlPropEnum$ for an enumerated property's choices. "Form" and "Menu" stand for the form itself and a menu item. The event list is CtlEventList$, and CtlEventParams$ writes an event procedure's parameter list; CtlTypeEventParams$(typeName, eventName) does it for one type, whose events may have parameters of their own (a UserControl's events), and "UserControl" stands for the object in a UserControl's own code (UserControl_MouseDown has Button, Shift, X, and Y). For a project's files, FrmFormName$ and FrmControlNames$ read a .frm file's form and control names (the Object list), FrmCode$ its code, and ClsClassName$ a class module's name.

    The form being designed is a real form: CreateControl and RemoveControl add and remove controls, CallByName reads and sets a property by its name, Me.Controls lists the controls, Container (Set ctl.Container = Frame1) and MoveBefore move them, and LoadFormText shows a .frm file. Its DesignMode (see the Form control) stops the controls from working and reports the mouse and keys in DesignMouse and DesignKey, with the selection handles drawn for DesignSelection.

    -

    A project's own UserControls go on the toolbox too, as in the IDE. Compile the project -- its forms may be left out -- and give the module and the source set to CompDesignControls(module, src, appDir) from compiler.bas: the UserControls are then control types of this program alone, listed by CtlTypeName$ with their Description as the tooltip and their ToolboxIcon as the icon, with their properties, methods and events, and known to CreateControl and LoadFormText. On the forms they run their code -- not the project's module-level code -- with UserControl.UserMode False on a form in DesignMode; App.Path is appDir, or the project's folder for "". When a control's code calls UserControl.PropertyChanged, the form raises DesignPropertyChanged (CtrlName, PropertyName), so the properties window can read them again. Call CompDesignControls again after a UserControl changed (the ones before are let go first) and CompDesignRelease when the project closes; the end of the program lets them go too. The module stays in use while they are there, even after CompModuleFree, and must be one this program compiled.

    +

    A project's own UserControls go on the toolbox too, as in the IDE. Compile the project -- its forms may be left out -- and give the module and the source set to CompDesignControls(module, src, appDir) from compiler.bas: the UserControls are then control types of this program alone, listed by CtlTypeName$ with their Description as the tooltip and their ToolboxIcon as the icon, with their properties, methods, and events, and known to CreateControl and LoadFormText. On the forms they run their code -- not the project's module-level code -- with UserControl.UserMode False on a form in DesignMode; App.Path is appDir, or the project's folder for "". When a control's code calls UserControl.PropertyChanged, the form raises DesignPropertyChanged (CtrlName, PropertyName), so the properties window can read them again. Call CompDesignControls again after a UserControl changed (the ones before are let go first) and CompDesignRelease when the project closes; the end of the program lets them go too. The module stays in use while they are there, even after CompModuleFree, and must be one this program compiled.

    ' compiler.bas and designer.bas in the project; Canvas is the form
     ' being designed
     Sub LoadProjectControls(dbpPath As String)
    @@ -5553,10 +5676,10 @@ End Sub
     Sub Canvas_DesignPropertyChanged(CtrlName As String, PropertyName As String)
         If CtrlName = Canvas.DesignSelection Then RefreshPropertyList
     End Sub
    -

    A .frm file can also be worked on as data, the way the DVX BASIC IDE does it, without showing it. FrmOpen(text$), FrmOpenFile(path$) or FrmNew(name$) gives a handle to a form model (0 when the text is not a form); FrmText$ writes it back as .frm text in the form the IDE saves and FrmSave writes it to a file; FrmFree frees it (a program's open handles are freed when it ends). FrmGet$ and FrmSet read and change the form's properties (Caption, Width, AutoSize, Layout, Icon, Tag ...; Name renames the form), FrmGetCode$ and FrmSetCode its code. The controls are numbered from 0 in file order, each one followed by the controls inside it: FrmCtlCount, FrmCtlFind, FrmCtlName$, FrmCtlType$, FrmCtlIndex (its control array index, -1 when it is not in an array) and FrmCtlParent (the container, -1 for the form) describe them; FrmCtlGet$ and FrmCtlSet read and set any property as text (Name renames the control and its whole control array, the controls inside it follow; Index makes it an array member; an OptionButton set to True clears the others in its container), FrmCtlRemoveProp takes a set one out, and FrmCtlPropCount / FrmCtlPropName$ list the ones that were set. FrmCtlAdd adds a control (its name "" gives the next free one, Command1, Label2 ...) as the last control of a container (-1: the form), FrmCtlRemove removes one with what is inside it, FrmCtlMove puts one before another of the same container and FrmCtlReparent into another container. FrmCtlText$ gives a control with what is inside it as .frm text, what a designer puts on the Clipboard to copy it, and FrmCtlPaste puts such text into a container: a name that is already taken is replaced by a free one. The menu items are numbered from 0 in menu order with a Level (0 for a menu on the menu bar or a popup menu, 1 for its items, 2 and more for submenus): FrmMenuCount, FrmMenuGet$ / FrmMenuSet (Name, Caption, Shortcut, ShortcutText, Checked, RadioCheck, Enabled, Visible, Level), FrmMenuAdd, FrmMenuRemove (with the items under it), FrmMenuMove (past the previous or next item of its level) and FrmMenuIndent (one level in or out, with the items under it). Only what was set is written: a property a control has by default is left out of the file.

    +

    A .frm file can also be worked on as data, the way the DVX BASIC IDE does it, without showing it. FrmOpen(text$), FrmOpenFile(path$), or FrmNew(name$) gives a handle to a form model (0 when the text is not a form); FrmText$ writes it back as .frm text in the form the IDE saves and FrmSave writes it to a file; FrmFree frees it (a program's open handles are freed when it ends). FrmGet$ and FrmSet read and change the form's properties (Caption, Width, AutoSize, Layout, Icon, Tag ...; Name renames the form), FrmGetCode$ and FrmSetCode its code. The controls are numbered from 0 in file order, each one followed by the controls inside it: FrmCtlCount, FrmCtlFind, FrmCtlName$, FrmCtlType$, FrmCtlIndex (its control array index, -1 when it is not in an array), and FrmCtlParent (the container, -1 for the form) describe them; FrmCtlGet$ and FrmCtlSet read and set any property as text (Name renames the control and its whole control array, the controls inside it follow; Index makes it an array member; an OptionButton set to True clears the others in its container), FrmCtlRemoveProp takes a set one out, and FrmCtlPropCount / FrmCtlPropName$ list the ones that were set. FrmCtlAdd adds a control (its name "" gives the next free one, Command1, Label2 ...) as the last control of a container (-1: the form), FrmCtlRemove removes one with what is inside it, FrmCtlMove puts one before another of the same container, and FrmCtlReparent into another container. FrmCtlText$ gives a control with what is inside it as .frm text, what a designer puts on the Clipboard to copy it, and FrmCtlPaste puts such text into a container: a name that is already taken is replaced by a free one. The menu items are numbered from 0 in menu order with a Level (0 for a menu on the menu bar or a popup menu, 1 for its items, 2 and more for submenus): FrmMenuCount, FrmMenuGet$ / FrmMenuSet (Name, Caption, Shortcut, ShortcutText, Checked, RadioCheck, Enabled, Visible, Level), FrmMenuAdd, FrmMenuRemove (with the items under it), FrmMenuMove (past the previous or next item of its level), and FrmMenuIndent (one level in or out, with the items under it). Only what was set is written: a property a control has by default is left out of the file.

    FormText$(formName$) writes a loaded form as it is now as .frm text -- one shown with LoadFormText, made with CreateForm and CreateControl, or one of the program's own: its properties, its controls in their containers with each property that differs from a new control of that type, and its menus (a form loaded this way has no code). A designer that keeps its form live saves it with FormText$, and FrmOpen(FormText$(...)) reads it as a model.

    -

    compiler.bas reads source text as the compiler does, for a code editor: CompTokens$(source) lists its tokens (line, column, length, kind and text, the kind one of Keyword, Identifier, String, Number, Date, Symbol, Comment and Error); CompInCode(source, position) tells whether a character is code rather than inside a string or a comment; CompRenameInCode$(source, oldName, newName, codeOnly) renames a form, control or menu item where code refers to it (Command1.Caption, Command1_Click), leaving strings and comments alone with codeOnly; CompWithEvents$ lists the WithEvents variables a file declares and CompClassEvents$ the Events of a class module, for the Object and Event lists; CompHelpTopic$(word) and CompHelpCtrlTopic$(typeName) are the help topics F1 opens (HelpViewTopic on dvxbasic.hlp beside the IDE).

    -

    A code editor is a TextArea: SetSyntaxMode "bas" colours BASIC as the IDE does (SetSyntaxColor changes the colours), SetLineMarker marks breakpoints and the current line, GutterClick reports clicks on the line numbers, SelStart / SelLength / SelText and Cut, Copy, Paste, SelectAll and Undo give the Edit menu, FindNext / ReplaceSelection / ReplaceAll find and replace, and LineCount / GetLine / CursorLine / CursorCol read the text (see TextArea).

    +

    compiler.bas reads source text as the compiler does, for a code editor: CompTokens$(source) lists its tokens (line, column, length, kind, and text, the kind one of Keyword, Identifier, String, Number, Date, Symbol, Comment, and Error); CompInCode(source, position) tells whether a character is code rather than inside a string or a comment; CompRenameInCode$(source, oldName, newName, codeOnly) renames a form, control, or menu item where code refers to it (Command1.Caption, Command1_Click), leaving strings and comments alone with codeOnly; CompWithEvents$ lists the WithEvents variables a file declares and CompClassEvents$ the Events of a class module, for the Object and Event lists; CompHelpTopic$(word) and CompHelpCtrlTopic$(typeName) are the help topics F1 opens (HelpViewTopic on dvxbasic.hlp beside the IDE).

    +

    A code editor is a TextArea: SetSyntaxMode "bas" colours BASIC as the IDE does (SetSyntaxColor changes the colours), SetLineMarker marks breakpoints and the current line, GutterClick reports clicks on the line numbers, SelStart / SelLength / SelText and Cut, Copy, Paste, SelectAll, and Undo give the Edit menu, FindNext / ReplaceSelection / ReplaceAll find and replace, and LineCount / GetLine / CursorLine / CursorCol read the text (see TextArea).

    ' designer.bas in the project: put a copy of the first control of a
     ' .frm file into a Frame of the same form and save it
     Sub CopyIntoFrame(path As String)
    @@ -5604,16 +5727,24 @@ End Sub
    Enabled Boolean R/W Whether the control accepts user input. Disabling a container (Frame, VBox, HBox...) also blocks mouse and keyboard input to every control inside it. BackColor Long R/W Background color as a 24-bit RGB value packed in a Long (use the RGB function to construct). ForeColor Long R/W Foreground (text) color as a 24-bit RGB value packed in a Long (use the RGB function to construct). + FontName String R/W The family of the font the control draws its text in, such as "DejaVu Sans" ("System" is DVX's own 8 x 16 font). A family DVX does not have gives the System font. See Fonts below. + FontSize Integer R/W The size of the font in pixels. A size the family does not come in becomes the nearest one it has. + FontBold Boolean R/W True for the bold style of the font. + FontItalic Boolean R/W True for the italic style of the font. + FontUnderline Boolean R/W True to underline the text. + FontStrikethru Boolean R/W True to draw a line through the text. TabIndex Integer R Accepted for VB compatibility but ignored. DVX has no tab order. ToolTipText String R/W Tooltip text shown after the mouse hovers over the control. Empty string or unset = no tooltip. HelpTopic String R/W Help topic opened by F1 while the control has the focus. Empty = the form's HelpTopic. F1 uses the project's help file. DragMode Integer R/W vbManual (0, the default): the program starts a drag with the Drag method. vbAutomatic (1): pressing the left mouse button on the control starts a drag. See Drag and Drop below. - DragIcon Picture R/W Picture shown under the mouse while the control is dragged: a picture file name (BMP, PNG, JPEG or GIF) or a Picture object from LOADPICTURE (Set works too). Reading it gives a Picture object, which stands for the file name where a value is needed. Empty = a dotted outline the size of the control. + DragIcon Picture R/W Picture shown under the mouse while the control is dragged: a picture file name (BMP, PNG, JPEG, or GIF) or a Picture object from LOADPICTURE (Set works too). Reading it gives a Picture object, which stands for the file name where a value is needed. Empty = a dotted outline the size of the control. Parent Form R The form the control is on (Text1.Parent.Caption). Tag String R/W Any text the program wants to keep with the control (DVX does nothing with it), as in Visual Basic. Also in the .frm file. - MousePointer Integer R/W The mouse pointer shown over the control (and the controls inside it that have none of their own): vbDefault (0, the control's own pointer), vbArrow (1), vbCrosshair (2), vbSizeNESW (6), vbSizeNS (7), vbSizeNWSE (8), vbSizeWE (9), vbHourglass (11) or vbNoDrop (12). A form's MousePointer is used where no control has one, and Screen.MousePointer over every form of the program. Change it in MouseMove to show different pointers over parts of a control. Also in the .frm file; another value is an error. + MousePointer Integer R/W The mouse pointer shown over the control (and the controls inside it that have none of their own): vbDefault (0, the control's own pointer), vbArrow (1), vbCrosshair (2), vbSizeNESW (6), vbSizeNS (7), vbSizeNWSE (8), vbSizeWE (9), vbHourglass (11), or vbNoDrop (12). A form's MousePointer is used where no control has one, and Screen.MousePointer over every form of the program. Change it in MouseMove to show different pointers over parts of a control. Also in the .frm file; another value is an error. Container Object R/W The container control the control is in (a Frame, VBox, TabPage ...), or its form. Set Text1.Container = Frame1 moves the control into Frame1, after the controls there; Set Text1.Container = Me moves it onto the form itself. The new container must be a container control on the same form, not the control itself or one inside it. -

    Size properties (Width, Height and the Min/Max sizes) are limited to 0..32767.

    +

    Size properties (Width, Height, and the Min/Max sizes) are limited to 0..32767.

    +

    Fonts

    +

    The six Font properties are one font. A control that has not been given one draws in the font of the control it is in (a Frame, for example), or else in its form's; the properties read the font the control draws with. Setting one of them gives the control a font of its own with that one part changed, keeping the others: Label1.FontBold = True makes the label's text bold in whatever family and size it had. Controls that edit text in columns (TextBox, TextArea, ComboBox, SpinButton, and Terminal) take only fixed-pitch families, such as "DejaVu Sans Mono", and show the System font in place of any other. The fonts DVX has, and the sizes each comes in, are listed by font.bas; DlgFont in commdlg.bas lets the user choose one. In the form designer the Font row of the Properties window sets all six, and they are saved in the .frm file.

    Default Properties

    As in Visual Basic, a control's name used where a value is needed means its default property: Text1 = "hi" sets Text1.Text, Print Label1 prints Label1.Caption, and If Check1 Then reads Check1.Value. Where an object is expected -- SET, Is, an object parameter, Collection.Add, TypeOf -- the name still means the control itself.

      Control                                               Default property
    @@ -5624,7 +5755,7 @@ End Sub
    HScrollBar, SpinButton, ProgressBar Value Image, ImageButton, PictureBox Picture Timer Enabled -

    Other controls, forms and menu items have no default property: using one as a value raises error 13.

    +

    Other controls, forms, and menu items have no default property: using one as a value raises error 13.

    Common Events

    These events are wired on every control, whether it is loaded from a .frm file or created at runtime with CreateControl (whose handlers SetEvent names).

      Event       Parameters                                    Description
    @@ -5635,7 +5766,7 @@ End Sub
    GotFocus (none) Fires when the control receives keyboard focus. LostFocus (none) Fires when the control loses keyboard focus. KeyPress KeyAscii As Integer Fires when a printable key is pressed. KeyAscii is the ASCII code. - KeyDown KeyCode As Integer, Shift As Integer Fires when any key is pressed down, after the control has handled it. KeyCode is the key (vbKeyDelete, vbKeyReturn, ... or a character's code; Ctrl+A to Ctrl+Z are 1 to 26); Shift holds vbShiftMask, vbCtrlMask and vbAltMask bits (see Key Code Constants in the language reference). + KeyDown KeyCode As Integer, Shift As Integer Fires when any key is pressed down, after the control has handled it. KeyCode is the key (vbKeyDelete, vbKeyReturn, ... or a character's code; Ctrl+A to Ctrl+Z are 1 to 26); Shift holds vbShiftMask, vbCtrlMask, and vbAltMask bits (see Key Code Constants in the language reference). KeyUp KeyCode As Integer, Shift As Integer Fires when a key is released. MouseDown Button As Integer, X As Integer, Y As Integer Fires when a mouse button is pressed over the control. MouseUp Button As Integer, X As Integer, Y As Integer Fires when a mouse button is released over the control. @@ -5654,7 +5785,7 @@ End Sub

    Drag and Drop

    Any control can be dragged onto another control or onto a form, as in Visual Basic. While a control is dragged, a dotted outline of it (or its DragIcon picture) follows the mouse, and the controls and forms it passes over get DragOver events. Releasing the mouse button drops it: the control or form under the mouse gets a DragDrop event, with the dragged control as Source. Pressing Esc cancels the drag.

    The mouse pointer shows where a drop can land: over a control or form that has a DragDrop event procedure it is the usual arrow; over one without, and outside the program's forms, it is the "no drop" sign (a circle with a line through it). The outline or DragIcon picture still follows the mouse either way.

    -

    With DragMode set to vbAutomatic, pressing the left mouse button on the control starts the drag, and the control gets no Click, MouseDown or MouseUp from the left button. With DragMode vbManual, start the drag yourself, usually in MouseDown. A drag started while the mouse button is up ends at the next click. During a drag no mouse events other than DragOver and DragDrop are raised.

    +

    With DragMode set to vbAutomatic, pressing the left mouse button on the control starts the drag, and the control gets no Click, MouseDown, or MouseUp from the left button. With DragMode vbManual, start the drag yourself, usually in MouseDown. A drag started while the mouse button is up ends at the next click. During a drag no mouse events other than DragOver and DragDrop are raised.

    Drop targets only react: nothing is moved or copied for you. Use Source in DragDrop to decide what to do, for example read its Name or Caption, or check what kind of control it is with TypeOf Source Is TextBox (see Operators).

    Note that vbCancel is the MsgBox Cancel value (2), which as a Drag action means vbEndDrag; cancel a drag with Drag 0.

    DragIcon takes a file name or a picture loaded with LOADPICTURE: Set Label1.DragIcon = LoadPicture("drag.bmp"). A missing file or one that is not a picture raises its error at LOADPICTURE; a file name that cannot be read when the drag starts shows the outline instead.

    @@ -5678,36 +5809,36 @@ End Sub

    Drawing on Forms and PictureBoxes

    Forms and PictureBoxes have Visual Basic's drawing methods. They draw with the same code as the QBasic graphics statements (see Console and Graphics in the language reference), in pixels unless ScaleMode or Scale says otherwise, with colours as Visual Basic RGB values, &HBBGGRR (RGB(255, 0, 0) or &HFF is red). A PictureBox draws on its picture; a form gets a drawing area the size of its window the first time it draws, placed with its controls like a PictureBox (so draw on a form that has no controls, or use a PictureBox beside them).

    -

    Drawings stay until they are drawn over or cleared. In a form's own code, write Me.Line, Me.Circle and so on: Line, Circle and PSet without an object are the QBasic graphics statements (they draw on the program's screen), and Print without an object is the PRINT statement.

    +

    Drawings stay until they are drawn over or cleared. In a form's own code, write Me.Line, Me.Circle, and so on: Line, Circle, and PSet without an object are the QBasic graphics statements (they draw on the program's screen), and Print without an object is the PRINT statement.

    Drawing Methods

      Method                                                          Description
       ------                                                          -----------
       Cls                                                             Clears the drawing to BackColor; CurrentX and CurrentY go to 0.
       PSet [Step](x, y) [, color]                                     Draws a point (DrawWidth wide).
    -  Line [[Step](x1, y1)]-[Step](x2, y2) [, color] [, B | BF]         Draws a line, a box (B) or a filled box (BF). A box without F is filled with FillColor when FillStyle is 0.
    -  Circle [Step](x, y), radius [, color [, start [, end [, aspect]]]]  Draws a circle, ellipse or arc, filled with FillColor when FillStyle is 0 (full circles).
    +  Line [[Step](x1, y1)]-[Step](x2, y2) [, color] [, B | BF]         Draws a line, a box (B), or a filled box (BF). A box without F is filled with FillColor when FillStyle is 0.
    +  Circle [Step](x, y), radius [, color [, start [, end [, aspect]]]]  Draws a circle, ellipse, or arc, filled with FillColor when FillStyle is 0 (full circles).
       DrawFocusRect x, y, width, height [, color]                     Draws the dotted rectangle DVX shows around the control that has the keyboard focus (every other pixel of its edge), for things a program draws itself, such as the focused link of a help topic. A negative width or height counts from the other edge.
    -  Point(x, y)                                                     The colour of a pixel, -1 outside the picture. With fewer than 16 million colours on the screen it is the colour the screen shows: black, white and full colours such as RGB(255, 0, 0) read back exactly, others may differ slightly.
    -  Print items                                                     Writes text at CurrentX, CurrentY in ForeColor, FontScale times as large, with PRINT's ; , SPC and TAB; ends by moving to the next line unless the last item is followed by ; or ,.
    -  PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight [, opcode]]]   Copies a picture, stretched to width x height: an image file (BMP, PNG, JPEG or GIF), a picture from LOADPICTURE, or a PictureBox given by its name as a string or by a Control variable. opcode combines each pixel with the one already there (see Raster Operations).
    +  Point(x, y)                                                     The colour of a pixel, -1 outside the picture. With fewer than 16 million colours on the screen it is the colour the screen shows: black, white, and full colours such as RGB(255, 0, 0) read back exactly, others may differ slightly.
    +  Print items                                                     Writes text at CurrentX, CurrentY in ForeColor, in the object's font FontScale times as large, with PRINT's separators (; and ,), SPC, and TAB (counted in the font's character width); ends by moving to the next line unless the last item is followed by ; or ,.
    +  PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight [, opcode]]]   Copies a picture, stretched to width x height: an image file (BMP, PNG, JPEG, or GIF), a picture from LOADPICTURE, or a PictureBox given by its name as a string or by a Control variable. opcode combines each pixel with the one already there (see Raster Operations).
       Scale [(x1, y1)-(x2, y2)]                                        Gives the drawing coordinates of its own: (x1, y1) is the top-left corner and (x2, y2) the bottom-right one (y2 below y1 makes y go up). Scale alone goes back to pixels.
       ScaleX(width [, fromScale [, toScale]])                         A width in fromScale converted to toScale (see Scales).
       ScaleY(height [, fromScale [, toScale]])                        A height in fromScale converted to toScale.
    -  TextWidth(text)                                                 Width of the text in the scale's units, at the FontScale size.
    -  TextHeight(text)                                                Height of a line of text in the scale's units, at the FontScale size.
    + TextWidth(text) Width of the text in the object's font, in the scale's units, at the FontScale size. + TextHeight(text) Height of a line of text in the object's font, in the scale's units, at the FontScale size.

    Drawing the DVX Look

    These methods draw the parts DVX's own controls are made of, in the colours of the current theme, for controls a program draws itself (a PictureBox that acts as a list or a toolbar). Coordinates and lengths are in the scale's units; colours are RGB values.

      Method                                                          Description
       ------                                                          -----------
       DrawBevel x, y, width, height [, sunken [, face [, border]]]    A raised box like a window or button edge, or with sunken True a sunken one like a text box's. face is the fill colour (the theme's face colour when left out; -1 leaves the inside as it is, to frame what is already drawn); border is the edge width in pixels (2).
       DrawButtonFace x, y, width, height [, pressed [, face]]         A button face, up or with pressed True pushed down.
    -  DrawArrow x, y, size, direction [, color]                       A solid triangle centred on x, y, size pixel rows tall, pointing dvxArrowUp (0), dvxArrowDown (1), dvxArrowLeft (2) or dvxArrowRight (3), in ForeColor unless a colour is given.
    +  DrawArrow x, y, size, direction [, color]                       A solid triangle centred on x, y, size pixel rows tall, pointing dvxArrowUp (0), dvxArrowDown (1), dvxArrowLeft (2), or dvxArrowRight (3), in ForeColor unless a colour is given.
       DrawScrollBar x, y, length, position, visible, total [, horizontal [, control]]   A scroll bar with its arrows and its thumb, for a list of total rows of which visible are shown from row position, with its top-left corner at x, y. It stands upright (16 pixels wide, length tall, arrows at the top and bottom) unless horizontal is True: then it lies across (length wide, 16 pixels tall, arrows at the left and right ends). With control True it is the narrower bar of a control such as ListBox (14 pixels).
    -  DrawScrollArrow x, y, size, direction                           One arrow button of a scroll bar, as at the ends of DrawScrollBar's bars: a square button size wide and tall (size in the scale's horizontal units) with its top-left corner at x, y and an arrow pointing dvxArrowUp, dvxArrowDown, dvxArrowLeft or dvxArrowRight, in the theme's scroll bar colours.
    -  ScrollBarClick(length, offset, position, visible, total [, horizontal [, control]])   The new position after a click on a scroll bar DrawScrollBar drew with the same length, rows, direction and kind: offset is how far the click is from the top of the bar, or from its left end when horizontal is True. The arrows move one row, the trough a page, the thumb leaves it as it is. Needs no drawing.
    -  ScrollBarsNeeded(contentWidth, contentHeight, areaWidth, areaHeight)   Which scroll bars content that big needs in an area that big, as DVX's controls decide it: vbSBNone (0), vbHorizontal (1), vbVertical (2) or vbBoth (3). A vertical bar narrows the area and may make a horizontal one needed, and the other way round. The sizes are in the object's scale. Needs no drawing.
    -  PrintAccel text [, enabled]                                     Prints a caption at CurrentX, CurrentY with the letter after & underlined (&& prints one &); enabled False gives the greyed, embossed look of a disabled control. CurrentX moves past the text.
    -  TextWidthAccel(text)                                            Width of a caption without its & signs, in the scale's units.
    +  DrawScrollArrow x, y, size, direction                           One arrow button of a scroll bar, as at the ends of DrawScrollBar's bars: a square button size wide and tall (size in the scale's horizontal units) with its top-left corner at x, y and an arrow pointing dvxArrowUp, dvxArrowDown, dvxArrowLeft, or dvxArrowRight, in the theme's scroll bar colours.
    +  ScrollBarClick(length, offset, position, visible, total [, horizontal [, control]])   The new position after a click on a scroll bar DrawScrollBar drew with the same length, rows, direction, and kind: offset is how far the click is from the top of the bar, or from its left end when horizontal is True. The arrows move one row, the trough a page, the thumb leaves it as it is. Needs no drawing.
    +  ScrollBarsNeeded(contentWidth, contentHeight, areaWidth, areaHeight)   Which scroll bars content that big needs in an area that big, as DVX's controls decide it: vbSBNone (0), vbHorizontal (1), vbVertical (2), or vbBoth (3). A vertical bar narrows the area and may make a horizontal one needed, and the other way round. The sizes are in the object's scale. Needs no drawing.
    +  PrintAccel text [, enabled]                                     Prints a caption in the object's font at CurrentX, CurrentY with the letter after & underlined (&& prints one &); enabled False gives the greyed, embossed look of a disabled control. CurrentX moves past the text.
    +  TextWidthAccel(text)                                            Width of a caption without its & signs in the object's font, in the scale's units.
       AccelKey(text)                                                  The access key of a caption in lower case ("f" for "&File"), "" when it has none.
    Sub Picture1_Paint()
         Picture1.DrawBevel 0, 0, Picture1.ScaleWidth, Picture1.ScaleHeight, True, vbWhite
    @@ -5725,7 +5856,7 @@ Sub Picture1_MouseDown(Button As Integer, X As Integer, Y As Integer)
             Picture1.Refresh
         End If
     End Sub
    -

    Without a colour, drawing uses ForeColor. Step makes a point relative to CurrentX, CurrentY, which every drawing method leaves at its last point. Coordinates, radii, CurrentX and CurrentY and PaintPicture's rectangles are in the scale's units (a radius in its horizontal units); DrawWidth is always in pixels. A colour or angle out of range stops the program with an error.

    +

    Without a colour, drawing uses ForeColor. Step makes a point relative to CurrentX, CurrentY, which every drawing method leaves at its last point. Coordinates, radii, CurrentX and CurrentY, and PaintPicture's rectangles are in the scale's units (a radius in its horizontal units); DrawWidth is always in pixels. A colour or angle out of range stops the program with an error.

    Raster Operations

    PaintPicture's opcode says how each copied pixel is combined with the pixel already there (the destination) and with the FillColor (the pattern), as in Visual Basic. Leaving it out copies the picture (vbSrcCopy).

      Constant        Value       Result
    @@ -5750,8 +5881,8 @@ End Sub
    Picture1.PaintPicture Picture2, 0, 0, 32, 32, 0, 0, 32, 32, vbSrcInvert   ' XOR the picture in
     Picture1.PaintPicture Picture2, 0, 0, 32, 32, 0, 0, 32, 32, vbSrcInvert   ' and out again

    Scales

    -

    ScaleMode picks the units: 0 user (set by Scale or the Scale properties), 1 twips (1440 an inch, 15 a pixel), 2 points (72 an inch), 3 pixels, 4 characters (120 twips across, 240 down), 5 inches, 6 millimetres, 7 centimetres. Unlike Visual Basic, whose default is twips, DVX starts every form and PictureBox in pixels (ScaleMode 3), and Scale without arguments goes back to pixels. Setting ScaleLeft, ScaleTop, ScaleWidth or ScaleHeight makes a user scale from the current one; ScaleWidth and ScaleHeight may not be 0. With a user scale the units stay fixed to the picture's edges when it is resized; other scale modes keep their unit size.

    -

    The constants vbUser (0), vbTwips (1), vbPoints (2), vbPixels (3), vbCharacters (4), vbInches (5), vbMillimeters (6) and vbCentimeters (7) name the scale modes. The units assume a standard display of 96 pixels an inch (15 twips a pixel), whatever the screen really is, so an inch is always 96 pixels.

    +

    ScaleMode picks the units: 0 user (set by Scale or the Scale properties), 1 twips (1440 an inch, 15 a pixel), 2 points (72 an inch), 3 pixels, 4 characters (120 twips across, 240 down), 5 inches, 6 millimetres, 7 centimetres. Unlike Visual Basic, whose default is twips, DVX starts every form and PictureBox in pixels (ScaleMode 3), and Scale without arguments goes back to pixels. Setting ScaleLeft, ScaleTop, ScaleWidth, or ScaleHeight makes a user scale from the current one; ScaleWidth and ScaleHeight may not be 0. With a user scale the units stay fixed to the picture's edges when it is resized; other scale modes keep their unit size.

    +

    The constants vbUser (0), vbTwips (1), vbPoints (2), vbPixels (3), vbCharacters (4), vbInches (5), vbMillimeters (6), and vbCentimeters (7) name the scale modes. The units assume a standard display of 96 pixels an inch (15 twips a pixel), whatever the screen really is, so an inch is always 96 pixels.

    ScaleX and ScaleY convert a width or a height from one scale to another, as in Visual Basic: fromScale is the scale the value is in, vbHimetric (8, hundredths of a millimetre, the unit of a Picture's Width and Height) when it is left out; toScale the scale wanted, the object's ScaleMode when left out. vbUser means the object's current scale. Visual Basic's vbContainerPosition and vbContainerSize are not available; any other scale number stops the program with an error.

    Picture1.Scale (0, 100)-(100, 0)     ' 0..100 across, y going up
     Picture1.Line (0, 0)-(100, 100), RGB(255, 0, 0)
    @@ -5776,7 +5907,7 @@ End Sub
    DrawWidth Integer 1 Pen width in pixels. FillStyle Integer 1 How boxes (B) and circles are filled with FillColor: 0 solid, 1 (the default) not at all, 2 horizontal lines, 3 vertical lines, 4 upward diagonals, 5 downward diagonals, 6 a cross hatch, 7 a diagonal cross hatch (the lines 8 pixels apart, the gaps left as they are). Other values are an error. BF fills solid in the line's colour. FillColor Long &H000000 Colour boxes and circles are filled with. - FontScale Integer 1 How many times as large Print writes its text (1 to 8): each dot of DVX's 8 x 16 font becomes a FontScale x FontScale block, for titles and banners. TextWidth and TextHeight follow it; PrintAccel does not. + FontScale Integer 1 How many times as large Print writes its text (1 to 8): each dot of the font becomes a FontScale x FontScale block, for titles and banners. TextWidth and TextHeight follow it; PrintAccel does not. AutoRedraw Boolean False True: no Paint events (see above). ScaleMode Integer 3 Units of the coordinates (see Scales). ScaleLeft Single 0 Coordinate of the left edge. @@ -5903,11 +6034,11 @@ End Caption String The text displayed. Use & for accelerator key. Set to "-" for a separator. Checked Boolean Whether the menu item shows a checkmark. An item with a Checked line (True or False) is a check item: choosing it turns its checkmark on or off before its Click event runs, so the handler reads the new state. RadioCheck Boolean Show the check as a radio dot instead of a checkmark. Radio items next to each other form a group: choosing one checks it and clears the others. - Shortcut String A key that runs the item from anywhere in the form, shown at the right of the item: "F2", "Ctrl+X", "Alt+Enter", "Shift+Del" (Ctrl, Alt and Shift, then a letter, digit, F1 to F12, or Backspace, Del, Down, End, Enter, Esc, Home, Ins, Left, PgDn, PgUp, Right, Space, Tab, Up). + Shortcut String A key that runs the item from anywhere in the form, shown at the right of the item: "F2", "Ctrl+X", "Alt+Enter", "Shift+Del" (Ctrl, Alt, and Shift, then a letter, digit, F1 to F12, or Backspace, Del, Down, End, Enter, Esc, Home, Ins, Left, PgDn, PgUp, Right, Space, Tab, Up). ShortcutText String Key text shown at the right of the item without making a shortcut, for keys a control handles itself (for example "Del" on a Delete item whose list deletes on Del in its KeyDown handler, so a text box keeps its own Del). Shown instead of the Shortcut's text when both are set. Enabled Boolean Whether the menu item is enabled (default True). Visible Boolean On a top-level menu, False makes it a popup menu (for ContextMenu) instead of a menu bar entry. On an item or separator inside a menu, False hides it until code sets Visible = True. -

    A Shortcut key works while the form's window has the focus, before the focused control sees the key, so give one only to keys no control there needs (F-keys, Ctrl or Alt combinations). F1 always opens the help and never reaches a shortcut. An unknown Shortcut stops the program with an error when the form loads.

    +

    A Shortcut key works while the form's window has the focus, before the focused control sees the key, so give one only to keys no control there needs (F-keys, Ctrl, or Alt combinations). F1 always opens the help and never reaches a shortcut. An unknown Shortcut stops the program with an error when the form loads.

    Begin Menu mnuRename
         Caption = "&Rename..."
         Shortcut = "F2"
    @@ -5923,7 +6054,7 @@ End
    Caption String R/W The item's text, with & before its access key. Setting it keeps the item's key text (Shortcut or ShortcutText), and reading it gives the text without it. Checked Boolean R/W Whether the menu item shows a checkmark. Setting it on an item that has neither Checked nor RadioCheck in the .frm makes it a check item (as in VB, any item can be checked). Enabled Boolean R/W Whether the menu item is enabled. - Visible Boolean R/W Whether the item (or separator) is in its menu. A hidden item keeps its caption, checkmark and place: shown again, it returns where it was. A submenu cannot be hidden (hide or remove its items, or set Enabled = False). + Visible Boolean R/W Whether the item (or separator) is in its menu. A hidden item keeps its caption, checkmark, and place: shown again, it returns where it was. A submenu cannot be hidden (hide or remove its items, or set Enabled = False). Shortcut String R/W The key that runs the item ("Ctrl+K", written as in the .frm file, "" for none). Setting it changes the key and the key text shown in the item; "" removes both. An unknown key is an error.

    RadioCheck and ShortcutText are design-time only: using them on a menu item in code is a compile error in the IDE and a runtime error (Unknown menu property) otherwise. The top-level menus of the menu bar (File, Edit) have no run-time properties.

    Menus at Run Time

    @@ -5934,7 +6065,7 @@ End CreateMenu menu$ Makes an empty popup menu of that name (nothing happens if one exists). AddMenu menu$, caption$ Adds a menu at the end of the form's menu bar, which appears with the first one (so a form made with CreateForm can have a menu bar). Add items to it by its name. The window keeps its size; the controls get the height the bar takes. RemoveMenu menu$ Removes a menu of the menu bar with all its items; with the last one the bar goes too. Not for popup menus (DestroyMenu) or items (RemoveMenuItem). - AddMenuItem menu$, item$, caption$ [, shortcut$] Adds an item at the end of the menu. Choosing it runs the Sub item_Click, and item.Caption, .Checked, .Enabled, .Visible and .Shortcut work as for .frm items. shortcut$ gives it a key, as Shortcut in the .frm file. + AddMenuItem menu$, item$, caption$ [, shortcut$] Adds an item at the end of the menu. Choosing it runs the Sub item_Click, and item.Caption, .Checked, .Enabled, .Visible, and .Shortcut work as for .frm items. shortcut$ gives it a key, as Shortcut in the .frm file. AddMenuRadioItem menu$, item$, caption$ [, checked] Adds a radio item: radio items next to each other form a group, and choosing one checks it and clears the others. AddMenuSeparator menu$ [, separator$] Adds a separator line at the end of the menu. With a name it can be hidden and removed like an item. AddSubMenu menu$, submenu$, caption$ Adds a submenu at the end of the menu; add items to it by its name. @@ -5965,7 +6096,7 @@ End Sub

    A level-0 menu that contains children becomes a top-level menu header. A non-level-0 menu that contains children becomes a submenu.

    Event Dispatch

    -

    Choosing a menu item (not a header, separator or submenu) fires MenuName_Click, whether it is on the menu bar or in a popup menu.

    +

    Choosing a menu item (not a header, separator, or submenu) fires MenuName_Click, whether it is on the menu bar or in a popup menu.

    Sub mnuOpen_Click ()
         MsgBox "Open was clicked"
     End Sub
    @@ -6107,12 +6238,13 @@ End Sub
    HelpTopic String "" Help topic opened by F1 while the form is active and the focused control has no HelpTopic of its own. Empty opens the project's help file at its default page. Padding Integer 4 Space in pixels between the window's edges and the controls (the content box's Padding); 0 is none. Spacing Integer 4 Space in pixels between one control and the next (the content box's Spacing); 0 is none. - WindowState Integer 0 vbNormal (0), vbMinimized (1, shown only as an icon at the bottom of the screen) or vbMaximized (2, filling the screen). Set on a hidden form (or in the .frm file), it takes effect when the form is shown; reading it then gives the state the form will have. + WindowState Integer 0 vbNormal (0), vbMinimized (1, shown only as an icon at the bottom of the screen), or vbMaximized (2, filling the screen). Set on a hidden form (or in the .frm file), it takes effect when the form is shown; reading it then gives the state the form will have. Controls Collection (none) The form's controls (read-only), in the order they were made: Me.Controls.Count, Me.Controls(1) (the first), Me.Controls("Text1"), For Each c In Me.Controls. Elements of a control array are in it too but have no name key. Each use makes a new Collection; changing it does not change the form. DesignMode Boolean False True puts the form in design mode, for a form designer: see Designing Forms below. Not in .frm files. - Icon Picture (DVX's) The picture shown for the window while it is minimized: a picture file name (BMP, PNG, JPEG or GIF) or a picture from LOADPICTURE (Set works too); "" puts DVX's back. Reading it gives a Picture object. Also in the .frm file. A file that is not a picture is an error. + Icon Picture (DVX's) The picture shown for the window while it is minimized: a picture file name (BMP, PNG, JPEG, or GIF) or a picture from LOADPICTURE (Set works too); "" puts DVX's back. Reading it gives a Picture object. Also in the .frm file. A file that is not a picture is an error. Tag String "" Any text the program wants to keep with the form, as for controls. MousePointer Integer 0 The mouse pointer over the form where its controls have none (see MousePointer under Common Properties). + FontName String "System" The form's font: the font of every control on it that has none of its own, and of the form's Print and TextWidth. FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru go with it, as for controls (see Fonts under Common Properties). Also in the .frm file. ActiveControl Control (none) The control of the form that has the keyboard focus (read-only), Nothing when none of them has it: Me.ActiveControl.SelText for an Edit menu's Copy. Screen.ActiveControl is the focused control of the whole program. Not in .frm files. DesignSelection String "" The name of the control (Text1, or Text1(2) in a control array) whose selection handles are drawn while the form is in design mode; "" for none. Not in .frm files.

    Maximizing a window, or restoring a maximized one, changes its size, so Resize fires. Minimizing does not change the size and fires no Resize. A minimized window keeps its maximized state: set WindowState = vbMaximized to bring it back filling the screen, vbNormal to bring it back at its normal size.

    @@ -6124,20 +6256,20 @@ frmTerminal.WindowState = vbMinimized ' starts as an icon Load (none) Fires after the form and all controls are created. This is the default event. Unload (none) Fires when the form is being closed or unloaded. QueryUnload Cancel As Integer Fires before Unload. Set Cancel = 1 to abort the close. - Resize (none) Fires when the window changes size: resized by the user, maximized or restored (by the user or WindowState). + Resize (none) Fires when the window changes size: resized by the user, maximized, or restored (by the user or WindowState). Activate (none) Fires when the window gains focus. Deactivate (none) Fires when the window loses focus. Paint (none) Fires when the form is shown or resized and on Refresh, unless AutoRedraw is True (see Drawing on Forms and PictureBoxes). DragDrop Source As Control, X As Integer, Y As Integer Fires when a dragged control is dropped on an empty part of the form. X and Y are where, in the form. - DragOver Source As Control, X As Integer, Y As Integer, State As Integer Fires while a dragged control moves over an empty part of the form. State is vbEnter, vbLeave or vbOver. + DragOver Source As Control, X As Integer, Y As Integer, State As Integer Fires while a dragged control moves over an empty part of the form. State is vbEnter, vbLeave, or vbOver. NewArgs (none) Fires when the running program is handed new arguments: COMMAND$ has changed (see New Arguments below). - DesignMouse Action As Integer, Button As Integer, X As Integer, Y As Integer, CtrlName As String, Handle As Integer In design mode: the mouse was pressed, moved, released or double-clicked in the form. See Designing Forms below. + DesignMouse Action As Integer, Button As Integer, X As Integer, Y As Integer, CtrlName As String, Handle As Integer In design mode: the mouse was pressed, moved, released, or double-clicked in the form. See Designing Forms below. DesignKey KeyCode As Integer, Shift As Integer In design mode: a key was pressed in the form (KeyCode and Shift as in KeyDown). DesignPropertyChanged CtrlName As String, PropertyName As String In design mode: the code of a UserControl on the form called PropertyChanged. See Designing Forms below.

    See Drag and Drop under Common Properties, Events, and Methods. While a control is dragged over a form with no DragDrop event procedure, the mouse pointer shows the "no drop" sign.

    New Arguments

    A program with a NewArgs event procedure on any of its forms can be handed new arguments while it runs, instead of DVX starting another copy of it. COMMAND$ (and COMMAND$(1), COMMAND$(2), ...) then give the new arguments, and NewArgs fires on every loaded form that has the procedure. When the arguments arrive while one of the program's event procedures is still running -- for example while it shows a message box or a dialog, or a form with vbModal -- they are held, and NewArgs fires once that procedure has finished; if several arrive meanwhile, only the last is given.

    -

    DVX hands new arguments to the help viewer: F1 and the Help menus of programs (and HelpView in BASIC) send "helpfile topic" to the program installed as APPS\KPUNCH\DVXHELP\DVXHELP.APP when it is already running. A help viewer written in BASIC and installed there gets them in NewArgs. Starting a program that is already running -- from Program Manager, the Run dialog, a document opened in it or ShellRun -- gives it the new arguments when it allows only one copy and has a NewArgs procedure: its window comes to the front and NewArgs fires. Without a NewArgs procedure DVX says it is already running; a program that allows more than one copy (Project Properties) starts another copy instead.

    +

    DVX hands new arguments to the help viewer: F1 and the Help menus of programs (and HelpView in BASIC) send "helpfile topic" to the program installed as APPS\KPUNCH\DVXHELP\DVXHELP.APP when it is already running. A help viewer written in BASIC and installed there gets them in NewArgs. Starting a program that is already running -- from Program Manager, the Run dialog, a document opened in it, or ShellRun -- gives it the new arguments when it allows only one copy and has a NewArgs procedure: its window comes to the front and NewArgs fires. Without a NewArgs procedure DVX says it is already running; a program that allows more than one copy (Project Properties) starts another copy instead.

    Sub Form1_NewArgs ()
         ' F1 in another program while this help viewer is open
         If COMMAND$(1) <> openFile Then OpenHelpFile COMMAND$(1)
    @@ -6155,7 +6287,7 @@ End Sub
    FormName.FitHeight Make the window just tall enough for its controls, keeping its width. FormName.SaveScreenshot file$ Save a picture of what the window shows inside its frame to a file: BMP when the name ends in .bmp, PNG otherwise. An error when the form has no window or the file cannot be written.

    Several Copies of a Form

    -

    A form can be open more than once, the way a program opens a second document window. New FormName makes another copy of the form: its own window, controls, menus and form-level variables, built from the same .frm. Its Load event runs, and it starts hidden like any loaded form, so call Show. Keep the copy in a variable declared As the form's name (or As Form, As Object):

    +

    A form can be open more than once, the way a program opens a second document window. New FormName makes another copy of the form: its own window, controls, menus, and form-level variables, built from the same .frm. Its Load event runs, and it starts hidden like any loaded form, so call Show. Keep the copy in a variable declared As the form's name (or As Form, As Object):

    Dim f As Form1          ' a Form1 copy, or the form itself
     Set f = New Form1       ' a new copy
     f.Caption = "Second window"
    @@ -6163,16 +6295,16 @@ f.Text1.Text = "Hello"  ' this copy's Text1
     f.Show
     
     Dim g As New Form1      ' declares and makes a copy at once
    -

    The code of the form serves every copy. In it, Me, the controls named on their own (Text1) and the form-level variables are those of the copy whose event is running, so each window keeps its own state. Unload f closes that copy only.

    +

    The code of the form serves every copy. In it, Me, the controls named on their own (Text1), and the form-level variables are those of the copy whose event is running, so each window keeps its own state. Unload f closes that copy only.

    The form's name (Form1.Caption, Form1.Show, Load Form1) always means the first copy loaded -- the one the program starts with -- as in Visual Basic. If that copy was unloaded, the name means the next one still open.

    Designing Forms

    A form designer written in BASIC shows the form being designed as a real form and turns on its design mode. While DesignMode is True, the controls on the form take no mouse clicks or keys and fire no events (Timers too are quiet); the form instead tells the designer what the user does:

      -
    • DesignMouse (Action, Button, X, Y, CtrlName, Handle): Action is DSGN_MOUSE_DOWN (1, a button was pressed), DSGN_MOUSE_MOVE (2), DSGN_MOUSE_UP (3) or DSGN_MOUSE_DBLCLICK (4, a double click: open the code, say). Button is the button pressed or released (1 left, 2 right, 4 middle; for a move, the buttons held). X and Y are where, in the form. CtrlName is the control there -- the innermost one when containers hold others -- or "" for the form itself. Handle is the selection handle of the selected control under the mouse: DSGN_HANDLE_E (3, the right edge: drag to change the width), DSGN_HANDLE_SE (4, the corner) or DSGN_HANDLE_S (5, the bottom edge), or DSGN_HANDLE_NONE (-1).
    • -
    • DesignKey (KeyCode, Shift): a key, for Delete, the arrow keys and so on.
    • +
    • DesignMouse (Action, Button, X, Y, CtrlName, Handle): Action is DSGN_MOUSE_DOWN (1, a button was pressed), DSGN_MOUSE_MOVE (2), DSGN_MOUSE_UP (3), or DSGN_MOUSE_DBLCLICK (4, a double click: open the code, say). Button is the button pressed or released (1 left, 2 right, 4 middle; for a move, the buttons held). X and Y are where, in the form. CtrlName is the control there -- the innermost one when containers hold others -- or "" for the form itself. Handle is the selection handle of the selected control under the mouse: DSGN_HANDLE_E (3, the right edge: drag to change the width), DSGN_HANDLE_SE (4, the corner), or DSGN_HANDLE_S (5, the bottom edge), or DSGN_HANDLE_NONE (-1).
    • +
    • DesignKey (KeyCode, Shift): a key, for Delete, the arrow keys, and so on.
    • DesignPropertyChanged (CtrlName, PropertyName): the code of a UserControl on the form -- one of the project's own, put on the designer's toolbox with CompDesignControls (compiler.bas) -- called UserControl.PropertyChanged PropertyName: read its properties again. Such controls run their code on the form with UserControl.UserMode False.
    -

    DesignSelection names the selected control; the form draws its eight selection handles over the controls (the three that resize in black) and shows the resize mouse pointers over them. The constants are in designer.bas, which also lists the control types, their properties and events (see Include Libraries in the language reference). Together with CreateControl, RemoveControl, CallByName, Controls, Container, MoveBefore, LoadFormText and the .frm model of designer.bas (FrmOpen, FrmText$, FrmCtlText$ / FrmCtlPaste to copy and paste controls, FormText$ to write the live form as .frm text), a designer has what the DVX BASIC IDE's own designer uses.

    +

    DesignSelection names the selected control; the form draws its eight selection handles over the controls (the three that resize in black) and shows the resize mouse pointers over them. The constants are in designer.bas, which also lists the control types, their properties, and events (see Include Libraries in the language reference). Together with CreateControl, RemoveControl, CallByName, Controls, Container, MoveBefore, LoadFormText, and the .frm model of designer.bas (FrmOpen, FrmText$, FrmCtlText$ / FrmCtlPaste to copy and paste controls, FormText$ to write the live form as .frm text), a designer has what the DVX BASIC IDE's own designer uses.

    Sub Canvas_DesignMouse(Action As Integer, Button As Integer, X As Integer, Y As Integer, CtrlName As String, Handle As Integer)
         If Action = DSGN_MOUSE_DOWN Then
             If Handle <> DSGN_HANDLE_NONE Then
    @@ -6186,7 +6318,7 @@ Dim g As New Form1      ' declares and makes a copy at once
    End If End Sub

    Drawing

    -

    Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, ScrollBarsNeeded, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, FontScale, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle and so on.

    +

    Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, ScrollBarsNeeded, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, FontScale, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle, and so on.

    Example

    Sub Form1_Load ()
         Form1.Caption = "Hello World"
    @@ -6208,7 +6340,7 @@ End Sub

    UserControls: Your Own Controls

    -

    A UserControl is a control you write in DVX BASIC. Once written, it is used exactly like the controls DVX comes with: you put it on a form in the designer (or in a .frm file with Begin), set its properties, call its methods and write handlers for its events. Programs written in C can create and use it too.

    +

    A UserControl is a control you write in DVX BASIC. Once written, it is used exactly like the controls DVX comes with: you put it on a form in the designer (or in a .frm file with Begin), set its properties, call its methods, and write handlers for its events. Programs written in C can create and use it too.

    A UserControl lives in a .ctl file. In the IDE, File > New UserControl makes one (see UserControls in the IDE). The file starts with a header that describes the control, then holds the control's code. The code is a class: every control on a form is one object of the class, with its own copies of the class's variables.

    VERSION DVX 1.00
     Begin UserControl Gauge
    @@ -6260,13 +6392,13 @@ End Sub
    ToolboxIcon A 24 x 24 picture file in the project folder, shown in the designer's toolbox. Description The toolbox tooltip (its name when not given). DefaultEvent The event the designer opens when the control is double-clicked. -

    Properties, Methods and Events

    +

    Properties, Methods, and Events

    The control's public members are what programs use:

      -
    • Public Property Get / Let (or Set) pairs and Public variables are its properties. A property with only Property Get is read-only, with only Property Let write-only. A property whose type is Integer, Long or Byte is a whole number, Boolean is True / False, Single and Double (and Currency) are numbers, String, Date and Variant are text; a property of an object type, an array or a TYPE cannot be a property of a control.
    • -
    • A property declared As an ENUM is a choice of its members, as a built-in control's Alignment is: the designer's Properties window shows the member's name and a double-click steps to the next member, the form file stores the name (Look = Circle), and code sets it with a member, its number or its name as text (Shape1.Look = Circle, Shape1.Look = 1, Shape1.Look = "Circle"). This needs the members numbered 0, 1, 2 and so on, each number once, as an ENUM numbers them when no values are given; an ENUM with other values (Small = 10) makes a whole-number property.
    • +
    • Public Property Get / Let (or Set) pairs and Public variables are its properties. A property with only Property Get is read-only, with only Property Let write-only. A property whose type is Integer, Long, or Byte is a whole number, Boolean is True / False, Single and Double (and Currency) are numbers, String, Date, and Variant are text; a property of an object type, an array, or a TYPE cannot be a property of a control.
    • +
    • A property declared As an ENUM is a choice of its members, as a built-in control's Alignment is: the designer's Properties window shows the member's name and a double-click steps to the next member, the form file stores the name (Look = Circle), and code sets it with a member, its number, or its name as text (Shape1.Look = Circle, Shape1.Look = 1, Shape1.Look = "Circle"). This needs the members numbered 0, 1, 2, and so on, each number once, as an ENUM numbers them when no values are given; an ENUM with other values (Small = 10) makes a whole-number property.
    • Public Subs and Functions are its methods. They take any arguments; a Function's value is the method's result.
    • -
    • Public Events are its events. RaiseEvent sends one to the program that has the control: its handler CtrlName_EventName runs with the arguments, and a ByRef parameter the handler changes (a number, a Boolean or a String) comes back changed.
    • +
    • Public Events are its events. RaiseEvent sends one to the program that has the control: its handler CtrlName_EventName runs with the arguments, and a ByRef parameter the handler changes (a number, a Boolean, or a String) comes back changed.

    Names that every control already has (Name, Left, Top, Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight, Weight, Visible, Enabled, ReadOnly, BackColor, ForeColor, Caption, Text, ToolTipText, ContextMenu, HelpTopic, DataSource, DataField, Tag, MousePointer ...) belong to the form: a control's own property of such a name is not used. Give your properties other names (Title instead of Caption, Value or Level instead of Text).

    A property's default is the value it has after UserControl_Initialize ran. The form's .frm sets only the properties the designer user changed, through your Property Let, after Initialize.

    @@ -6289,7 +6421,7 @@ End Sub DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, ScrollBarsNeeded, PrintAccel, TextWidthAccel, AccelKey Drawing in DVX's own look (bevels, button faces, arrows, upright and horizontal scroll bars and their arrow buttons, captions with an underlined access key), as on a PictureBox. CurrentX, CurrentY, ForeColor, BackColor, FillColor, FillStyle, FontScale, DrawWidth, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight Drawing properties, as on a PictureBox.

    The drawing members work exactly as on a PictureBox (see Drawing on Forms and PictureBoxes). Write them with UserControl in front -- UserControl.Line, UserControl.Print -- as a form's code writes Me.Line: a drawing statement on its own (Line, Cls, Print) draws on the program's screen, not on the control.

    -

    The drawing stays until you draw again. UserControl_Paint runs when the control first appears, after its size changed, when you call Refresh and after you call Invalidate; draw everything there and call UserControl.Invalidate (or UserControl.Refresh, to see the result at once) whenever what it shows changes.

    +

    The drawing stays until you draw again. UserControl_Paint runs when the control first appears, after its size changed, when you call Refresh, and after you call Invalidate; draw everything there and call UserControl.Invalidate (or UserControl.Refresh, to see the result at once) whenever what it shows changes.

    The Control's Events

    The program runs these procedures of the control's code when they exist. Each is a Private Sub with exactly these parameters:

      Procedure                                                              When
    @@ -6309,8 +6441,8 @@ End Sub
    UserControl_KeyUp (KeyCode As Integer, Shift As Integer) A key went up. UserControl_GotFocus () It got the keyboard focus. UserControl_LostFocus () It lost the keyboard focus. -

    Button is vbLeftButton, vbRightButton or vbMiddleButton (in MouseMove, the buttons held). Shift holds vbShiftMask, vbCtrlMask and vbAltMask. X and Y are pixels from the control's top-left corner. The compiler reports a handler with other parameters.

    -

    These events are the control's own: the program does not see them. The program sees only the events the control raises. To let a program react to clicks, raise your own Click (Public Event Click, then RaiseEvent Click in UserControl_Click); an event named Click, DblClick, Change, KeyPress, KeyDown, KeyUp, MouseDown, MouseUp, MouseMove or Scroll reaches the program's CtrlName_Click (and so on) handler as for any control. Give those events Visual Basic's parameters: Button, Shift, X, Y for MouseDown, MouseUp and MouseMove; KeyCode, Shift for KeyDown and KeyUp; KeyAscii for KeyPress; Delta for Scroll. GotFocus and LostFocus reach the program by themselves.

    +

    Button is vbLeftButton, vbRightButton, or vbMiddleButton (in MouseMove, the buttons held). Shift holds vbShiftMask, vbCtrlMask, and vbAltMask. X and Y are pixels from the control's top-left corner. The compiler reports a handler with other parameters.

    +

    These events are the control's own: the program does not see them. The program sees only the events the control raises. To let a program react to clicks, raise your own Click (Public Event Click, then RaiseEvent Click in UserControl_Click); an event named Click, DblClick, Change, KeyPress, KeyDown, KeyUp, MouseDown, MouseUp, MouseMove, or Scroll reaches the program's CtrlName_Click (and so on) handler as for any control. Give those events Visual Basic's parameters: Button, Shift, X, Y for MouseDown, MouseUp, and MouseMove; KeyCode, Shift for KeyDown and KeyUp; KeyAscii for KeyPress; Delta for Scroll. GotFocus and LostFocus reach the program by themselves.

    Using Controls in a Program

    In the IDE, File > New UserControl (or File > Add File with a .ctl file) adds a control to the open project; while the project is open the control is in the Toolbox, ready to be placed on the project's forms. In the project file, the .ctl file is listed in the [Controls] section of the project's .dbp:

    [Controls]
    @@ -6336,7 +6468,7 @@ Count = 1

    Limits

    • A control cannot hold other controls; it draws what it shows on its UserControl surface.
    • -
    • Properties are numbers, Booleans, text and ENUM choices; a property of an ENUM whose members are not numbered 0, 1, 2 ... is a whole number in the Properties window.
    • +
    • Properties are numbers, Booleans, text, and ENUM choices; a property of an ENUM whose members are not numbered 0, 1, 2 ... is a whole number in the Properties window.
    • CanGetFocus is fixed by the header (read-only in code).
    • UserControl_Terminate cannot draw: the surface is already gone.
    • A program cannot have a control of the same name as one installed in WIDGETS (the program stops with "Control cannot be registered").
    • @@ -6350,6 +6482,7 @@ Count = 1

      Terminal

      DVX Extension -- DVX Widget: ansiterm

      A VT100/ANSI terminal emulator with a text-mode cell buffer, 16-color CGA palette, and scrollback history. Processes standard ANSI escape sequences (cursor control, erase, scroll, SGR colors, DEC private modes) typically used by DOS BBS software. Default size is 80 columns by 25 rows. Supports text selection with the mouse, Ctrl+C to copy, and Ctrl+V to send clipboard text as input. Shift+PgUp and Shift+PgDn page through the scrollback history and the mouse wheel scrolls it; any other key is sent to the host and returns the view to the live screen.

      +

      Its font must be fixed-pitch, every character the same width; given a proportional font it draws in the system font instead. A fixed-pitch font of any size changes the size of every cell.

      Type-Specific Properties

        Property     Type      Description
         ----------   -------   -------------------------------------------
      @@ -6368,7 +6501,7 @@ Count = 1
      Write text$ Write a string to the terminal with full ANSI escape processing.

      No default event. Uses common events only.

      Serial Communication

      -

      The conn.bas library connects a Terminal the way the Terminal program does -- a serial port, a modem, the secure link or Telnet -- with capture, answerback and file transfers (ConnAttach; see Include Libraries). For a packet link of your own, use the comm.bas library:

      +

      The conn.bas library connects a Terminal the way the Terminal program does -- a serial port, a modem, the secure link, or Telnet -- with capture, answerback, and file transfers (ConnAttach; see Include Libraries). For a packet link of your own, use the comm.bas library:

      Dim link As Long
       link = CommOpen(1, 115200, 8, "N", 1, SER_HANDSHAKE_NONE)
       If link <> 0 Then
      @@ -6400,7 +6533,7 @@ End Sub
      Caption String The title displayed in the frame border.
        Property    Type      Description
         --------    -------   -------------------------------------------
      -  Alignment   Enum      Where the children sit along the frame from top to bottom when they do not fill it: Left (the top, the default), Center or Right (the bottom).
      +  Alignment   Enum      Where the children sit along the frame from top to bottom when they do not fill it: Left (the top, the default), Center, or Right (the bottom).
         Padding     Integer   Space in pixels between the border and the children: 4 unless set; 0 is none.
         Spacing     Integer   Space in pixels between one child and the next: none unless set.

      No type-specific methods or events.

      @@ -6426,7 +6559,7 @@ End

      Type-Specific Properties

        Property    Type      Description
         --------    -------   -------------------------------------------
      -  Alignment   Enum      Where the children sit along the row when they do not fill it: Left (the left, the default), Center or Right (the right).
      +  Alignment   Enum      Where the children sit along the row when they do not fill it: Left (the left, the default), Center, or Right (the right).
         Padding     Integer   Space in pixels between the edge of the box and the children: 4 unless set; 0 is none.
         Spacing     Integer   Space in pixels between one child and the next: 4 unless set; 0 is none.

      Alignment = Right puts a row of buttons at the right, as dialog boxes have them.

      @@ -6453,7 +6586,7 @@ End

      Type-Specific Properties

        Property    Type      Description
         --------    -------   -------------------------------------------
      -  Alignment   Enum      Where the children sit along the column when they do not fill it: Left (the top, the default), Center or Right (the bottom).
      +  Alignment   Enum      Where the children sit along the column when they do not fill it: Left (the top, the default), Center, or Right (the bottom).
         Padding     Integer   Space in pixels between the edge of the box and the children: 4 unless set; 0 is none.
         Spacing     Integer   Space in pixels between one child and the next: 4 unless set; 0 is none.

      No type-specific methods or events.

      @@ -6500,7 +6633,7 @@ End Sub

      PictureBox

      VB Equivalent: PictureBox -- DVX Widget: canvas | Name Prefix: Picture

      -

      A drawing surface (canvas). Supports drawing lines, rectangles, circles, text, and individual pixels. Can load BMP, PNG, JPEG and GIF images and save BMP or PNG images. The default canvas size is 64x64 pixels.

      +

      A drawing surface (canvas). Supports drawing lines, rectangles, circles, text, and individual pixels. Can load BMP, PNG, JPEG, and GIF images and save BMP or PNG images. The default canvas size is 64x64 pixels.

      Colors are Visual Basic RGB values, &HBBGGRR, as RGB() makes them (RGB(255, 0, 0) or &HFF is red).

      Type-Specific Properties

        Property      Type      Description
      @@ -6518,8 +6651,8 @@ End Sub
      DrawText x%, y%, text$ Draw text at the given position. FillCircle cx%, cy%, radius%, color% Draw a filled circle. FillRect x%, y%, w%, h%, color% Draw a filled rectangle. - GetPixel(x%, y%) Returns the color at a pixel. With fewer than 16 million colors on the screen it is the color the screen shows: black, white and full colors such as RGB(255, 0, 0) read back exactly, others may differ slightly. - Load(path$) Load a BMP, PNG, JPEG or GIF image; the canvas takes the image's size. Returns 0 on success, -1 on failure. + GetPixel(x%, y%) Returns the color at a pixel. With fewer than 16 million colors on the screen it is the color the screen shows: black, white, and full colors such as RGB(255, 0, 0) read back exactly, others may differ slightly. + Load(path$) Load a BMP, PNG, JPEG, or GIF image; the canvas takes the image's size. Returns 0 on success, -1 on failure. Resize w%, h% Resize the canvas to new dimensions. The drawing is cleared to white. Save(path$) Save the canvas: BMP when path$ ends in .bmp, PNG otherwise. Returns 0 on success, -1 on failure. SetPenColor color% Set the drawing color for subsequent operations. @@ -6569,6 +6702,7 @@ End Sub

      ComboBox

      VB Equivalent: ComboBox -- DVX Widget: combobox (editable text field + drop-down list, default maxLen 256)

      A combination of a single-line text input and a drop-down list. The user can either type a value or pick one from the list. Selecting a list item copies its text into the edit field. Supports the same AddItem / RemoveItem / Clear / List methods as ListBox.

      +

      Its font must be fixed-pitch, every character the same width; the designer's Font dialog offers only those families for it. Given a proportional font it draws in the system font instead.

      Type-Specific Properties

        Property    Type      Description
         ---------   -------   -------------------------------------------
      @@ -6603,7 +6737,7 @@ End Sub

      Data

      VB Equivalent: Data -- DVX Widget: data | Name Prefix: Data

      -

      A data access control that connects to a SQLite database and provides record navigation. Other controls bind to a Data control via their DataSource and DataField properties. The Data control cache is populated by calling Refresh. Move operations and Refresh auto-save the current row if dirty; a move is refused while the save fails, and Refresh discards an edit that cannot be saved. Updates find the record by the key value it was loaded with, so the key column itself can be edited; a key stored as a number in a column without a declared type, or a missing (NULL) key, is still found as long as it identifies exactly one record. Writing a field back with the value it already has is not an edit and does not fire Validate. While a Validate handler runs, MoveFirst, MoveLast, MoveNext, MovePrevious, AddNew, Refresh and Update on the same Data control are ignored (they would save, and validate, the same record again); Delete is allowed. A RecordSource that is a SELECT query is read-only, and edits made to its records are dropped when the current record changes.

      +

      A data access control that connects to a SQLite database and provides record navigation. Other controls bind to a Data control via their DataSource and DataField properties. The Data control cache is populated by calling Refresh. Move operations and Refresh auto-save the current row if dirty; a move is refused while the save fails, and Refresh discards an edit that cannot be saved. Updates find the record by the key value it was loaded with, so the key column itself can be edited; a key stored as a number in a column without a declared type, or a missing (NULL) key, is still found as long as it identifies exactly one record. Writing a field back with the value it already has is not an edit and does not fire Validate. While a Validate handler runs, MoveFirst, MoveLast, MoveNext, MovePrevious, AddNew, Refresh, and Update on the same Data control are ignored (they would save, and validate, the same record again); Delete is allowed. A RecordSource that is a SELECT query is read-only, and edits made to its records are dropped when the current record changes.

      See Data Binding for details

      Type-Specific Properties

        Property       Type      Description
      @@ -6708,7 +6842,7 @@ End Sub

      DropDown

      DVX Extension -- DVX Widget: dropdown (non-editable drop-down list)

      -

      A read-only drop-down list. Unlike ComboBox, the user cannot type free text; they can only select from the provided items. Supports AddItem / RemoveItem / Clear / List. Use the Up and Down keys to change the choice. F4, Alt+Down, Space or Enter opens the list; in the open list use the arrow keys to move and Enter (or F4, Alt+Down) to pick. Esc closes the list without changing the choice.

      +

      A read-only drop-down list. Unlike ComboBox, the user cannot type free text; they can only select from the provided items. Supports AddItem / RemoveItem / Clear / List. Use the Up and Down keys to change the choice. F4, Alt+Down, Space, or Enter opens the list; in the open list use the arrow keys to move and Enter (or F4, Alt+Down) to pick. Esc closes the list without changing the choice.

      Type-Specific Properties

        Property    Type      Description
         ---------   -------   -------------------------------------------
      @@ -6742,7 +6876,7 @@ End Sub

      HelpPane

      VB Equivalent: (none) -- DVX Widget: helpview

      -

      Shows one topic of a help file the way the DVX Help Viewer does: headings, paragraphs and lists wrapped to the control's width, notes, code, tables, pictures, rules and links, with a scroll bar when the topic is longer than the control. Open the help file with hlpfile.bas (HlpOpen) and give its handle to ShowTopic. With a TreeView for the table of contents and a few buttons, a HelpPane makes a complete help viewer (see Help Files under Include Libraries).

      +

      Shows one topic of a help file the way the DVX Help Viewer does: headings, paragraphs, and lists wrapped to the control's width, notes, code, tables, pictures, rules, and links, with a scroll bar when the topic is longer than the control. Open the help file with hlpfile.bas (HlpOpen) and give its handle to ShowTopic. With a TreeView for the table of contents and a few buttons, a HelpPane makes a complete help viewer (see Help Files under Include Libraries).

      A link is followed with a click, or with Enter or Space after Tab has moved to it: the HelpPane fires LinkClick with the ID of the topic the link names and remembers it in LinkTopic. It does not change topic by itself -- the LinkClick event procedure usually shows the topic and adds it to its Back list. Everything a topic shows is copied when it is shown, so closing the help file afterwards does not change what the HelpPane shows.

      Type-Specific Properties

        Property    Type      R/W   Description
      @@ -6785,7 +6919,7 @@ End Sub

      Type-Specific Properties

        Property      Type      Description
         -----------   -------   -------------------------------------------
      -  Picture       Picture   Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged.
      +  Picture       Picture   Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget, or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged.
         ImageWidth    Integer   Width of the loaded image in pixels (read-only).
         ImageHeight   Integer   Height of the loaded image in pixels (read-only).

      No type-specific methods or events.

      @@ -6812,7 +6946,7 @@ End Sub

      Type-Specific Properties

        Property      Type      Description
         -----------   -------   -------------------------------------------
      -  Picture       Picture   Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. A disabled Image is drawn grayed (its transparent color stays transparent).
      +  Picture       Picture   Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget, or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. A disabled Image is drawn grayed (its transparent color stays transparent).
         Stretch       Boolean   When True, an image larger than the control is scaled down to fit, keeping its proportions; a smaller image stays at natural size. When False, the image is shown at natural size.
         ImageWidth    Integer   Width of the loaded image in pixels (read-only).
         ImageHeight   Integer   Height of the loaded image in pixels (read-only).
      @@ -6913,7 +7047,7 @@ End Sub
      SelectAll Select all rows. SelectedRows$() Returns the numbers of the selected rows (as IsItemSelected numbers them) separated by spaces, "" when none: one call instead of IsItemSelected for every row of a long list. SetCell row%, col%, text$ Set the text of a cell. - SetColumns spec$ Define columns. Pipe-delimited "Name,Width,Align|Name,Width": the width in characters (missing or 0 = as wide as the contents), then Left (the default), Center or Right for the column's text. + SetColumns spec$ Define columns. Pipe-delimited "Name,Width,Align|Name,Width": the width in characters (missing or 0 = as wide as the contents), then Left (the default), Center, or Right for the column's text. SetItemSelected index%, selected Select or deselect a row. SetMultiSelect multi Enable or disable multi-select. SetReorderable reorderable Enable or disable row reordering. @@ -6965,10 +7099,118 @@ ListView2.SetRows "a.txt" & Chr$(9) & "120" & Ch

      Default Event: Click

      Common Properties, Events, and Methods

      +
      +

      RichEdit

      +

      VB Equivalent: RichTextBox -- DVX Widget: richedit

      +

      A text box for text in many fonts, sizes, and colors, bold, italic, underlined, or struck through, in paragraphs aligned left, centered, or right -- a small word processor's page. The user types, selects, and edits as in any text box; a program reads and changes the selection's look with the Sel properties, and the whole text, with its looks, as RTF (Rich Text Format, the format word processors save), so it can be saved to a file and loaded again.

      +

      Text starts in the control's own font (FontName, FontSize, and the other Font properties, or the form's) and in its ForeColor; text left in them follows the control when they change. The Sel properties read the first character of the selection; setting one changes the selection, or with nothing selected, the text typed next. SelStart counts characters from 0, and the break between two paragraphs is one character.

      +

      Type-Specific Properties

      +
        Property        Type      Description
      +  --------        ----      -----------
      +  TextRTF         String    The whole text with its fonts, styles, colors, and alignment, as RTF. Setting it replaces the text (plain text is taken as it is).
      +  SelRTF          String    The selection as RTF. Setting it replaces the selection.
      +  SelStart        Integer   Where the selection starts (the caret when nothing is selected).
      +  SelLength       Integer   How many characters are selected.
      +  SelText         String    The selection's text. Setting it replaces the selection with plain text.
      +  SelBold         Boolean   Bold.
      +  SelItalic       Boolean   Italic.
      +  SelUnderline    Boolean   Underlined.
      +  SelStrikethru   Boolean   Struck through.
      +  SelColor        Long      The color, as RGB() makes it; -1 for the control's ForeColor.
      +  SelFontName     String    The font family ("" for the control's own).
      +  SelFontSize     Integer   The size in pixels (the nearest one the family comes in).
      +  SelAlignment    Integer   The alignment of the selection's paragraphs: 0 left, 1 centered, 2 right.
      +  CanUndo         Boolean   An edit can be undone (read-only).
      +  CanRedo         Boolean   An undone edit can be done again (read-only).
      +

      Text (a common property) is the plain text, a line per paragraph; setting it replaces everything with plain text. ReadOnly lets the user select and copy but not change the text.

      +

      Type-Specific Methods

      +
        Method                 Description
      +  ------                 -----------
      +  Undo                   Undo the last edit (typing a run of characters is one edit). Up to 100 edits are kept.
      +  Redo                   Do an undone edit again.
      +  Cut, Copy, Paste       The clipboard. Text copied from a RichEdit pastes back into a RichEdit with its looks; text from elsewhere takes the look of the text typed next.
      +  SelectAll              Select all the text.
      +  Find(text)             Select the next place text is (any case), from the selection on and around past the end; returns where it starts, or -1.
      +  LoadFile(path)         Load a file: RTF when it is RTF, else plain text. Returns 0, or -1 when it cannot be read.
      +  SaveFile(path)         Save the text: as RTF when path ends in .rtf, else as plain text. Returns 0, or -1 when it cannot be written.
      +

      Type-Specific Events

      +
        Event        Parameters   Description
      +  -----        ----------   -----------
      +  SelChange    (none)       The selection or the caret moved.
      +

      Change fires after every edit. Default Event: Change

      +

      Keys

      +
        Key                         Action
      +  ---                         ------
      +  Ctrl+B, Ctrl+I, Ctrl+U      Bold, italic, or underline on or off.
      +  Ctrl+L, Ctrl+E, Ctrl+R      Align the paragraph left, centered, or right.
      +  Ctrl+Z, Ctrl+Y              Undo, Redo.
      +  Ctrl+X, Ctrl+C, Ctrl+V      Cut, Copy, Paste.
      +  Ctrl+A                      Select all.
      +  Ctrl+Left, Ctrl+Right       A word back or on.
      +  Ctrl+Home, Ctrl+End         The start or the end of the text.
      +  Shift with a moving key     Extend the selection.
      +

      A double click selects a word, a triple click the paragraph.

      +

      Example

      +
      Sub Form_Load ()
      +    RichEdit1.Text = "Dear friend,"
      +    RichEdit1.SelStart = 0
      +    RichEdit1.SelLength = 4
      +    RichEdit1.SelBold = True
      +    RichEdit1.SelColor = RGB(0, 0, 160)
      +End Sub
      +
      +Sub cmdItalic_Click ()
      +    RichEdit1.SelItalic = Not RichEdit1.SelItalic
      +End Sub
      +
      +Sub cmdSave_Click ()
      +    If RichEdit1.SaveFile("LETTER.RTF") <> 0 Then MsgBox "The letter could not be saved."
      +End Sub
      +

      Common Properties, Events, and Methods

      +
      +
      +

      RichLabel

      +

      DVX Widget: richlabel

      +

      Text that mixes fonts, sizes, bold and italic, underlining, and colors, in paragraphs that are each aligned left, centered, or right. Build the text with calls: SetFont, SetStyle, and SetColor choose how the text you add next looks; AddText adds it; AddLineBreak starts a new line; and AddParagraph starts a new paragraph. Clear empties the control so you can build it again.

      +

      Text starts in the control's own font (FontName, FontSize, and the other Font properties, or the form's). Set Width to have the text wrap at that width; otherwise each paragraph stays on one line and the control is as wide as its longest line.

      +

      Methods

      +
        Method                      Description
      +  ------                      -----------
      +  Clear                       Remove all the text. The font, style, and color go back to the control's own.
      +  SetFont family, size        The font family (for example "DejaVu Serif") and size in pixels of the text added next. "" and 0 go back to the control's own font and size.
      +  SetStyle style              FONT_BOLD, FONT_ITALIC, FONT_UNDERLINE, and FONT_STRIKE added together (from font.bas; 0 for plain) for the text added next.
      +  SetColor color              The color of the text added next, as RGB() makes it; -1 for the control's ForeColor.
      +  AddText text                Add text in the font, style, and color chosen.
      +  AddLineBreak                Start a new line in the same paragraph.
      +  AddParagraph alignment      Start a new paragraph: 0 left, 1 centered, 2 right (as a Label's Alignment).
      +

      Changing the font, style, or color affects only the text added afterwards. A line is as tall as the largest font on it.

      +

      No type-specific properties or events.

      +

      Default Event: Click

      +

      Example

      +
      Sub Form_Load ()
      +    RichLabel1.Clear
      +    RichLabel1.SetFont "DejaVu Serif", 18
      +    RichLabel1.SetStyle FONT_BOLD
      +    RichLabel1.AddText "Welcome"
      +    RichLabel1.AddParagraph 0
      +    RichLabel1.SetFont "", 0
      +    RichLabel1.SetStyle 0
      +    RichLabel1.AddText "Text in "
      +    RichLabel1.SetColor RGB(160, 0, 0)
      +    RichLabel1.AddText "red"
      +    RichLabel1.SetColor -1
      +    RichLabel1.AddText " and "
      +    RichLabel1.SetStyle FONT_ITALIC + FONT_UNDERLINE
      +    RichLabel1.AddText "italic underlined"
      +    RichLabel1.SetStyle 0
      +    RichLabel1.AddText " words."
      +End Sub
      +

      Common Properties, Events, and Methods

      +

      ScrollPane

      DVX Extension -- DVX Widget: scrollpane | Name Prefix: Scroll

      -

      A scrollable container. Place child controls inside and the ScrollPane automatically provides scrollbars when the content exceeds the visible area. Clicking on content that does not respond to the mouse (a Label, an Image or empty space) gives the ScrollPane the focus, so the arrow and page keys scroll it.

      +

      A scrollable container. Place child controls inside and the ScrollPane automatically provides scrollbars when the content exceeds the visible area. Clicking on content that does not respond to the mouse (a Label, an Image, or empty space) gives the ScrollPane the focus, so the arrow and page keys scroll it.

      Type-Specific Properties

        Property     Type      Description
         --------     -------   -------------------------------------------
      @@ -6985,7 +7227,7 @@ ListView2.SetRows "a.txt" & Chr$(9) & "120" & Ch
       

      Events

        Event    Description
         -----    -----------
      -  Change   The user scrolled the content (the scrollbars, dragging a scroll box, the arrow, page, Home and End keys); read ScrollTop and ScrollLeft for the new position. Setting ScrollTop or ScrollLeft, or ScrollToTop, does not fire it.
      + Change The user scrolled the content (the scrollbars, dragging a scroll box, the arrow, page, Home, and End keys); read ScrollTop and ScrollLeft for the new position. Setting ScrollTop or ScrollLeft, or ScrollToTop, does not fire it.

      Scrolling a Picture Bigger Than the Window

      A ScrollPane with NoBorder = True filling a form, holding a PictureBox larger than the window, gives the form scrollbars of its own: the user scrolls the picture, Change says where it is, and ScrollTop / ScrollLeft move it from code. To draw only the part in view (for a picture too large to keep whole), keep the PictureBox the size of the view, and draw from Change with ScrollTop and ScrollLeft as the offset.

      Sub Form1_Load ()
      @@ -7039,6 +7281,7 @@ End Sub

      SpinButton

      DVX Extension -- DVX Widget: spinner | Name Prefix: Spin

      A numeric input with up/down buttons. Supports integer mode (default) and real-number mode with configurable decimal places. Up/Down arrows step by the step size, Page Up/Page Down by ten steps. Typed or pasted text is committed on Enter or when the control loses focus; Esc cancels the edit. Ctrl+Z undoes the last edit of the typed text; once the text is committed or reformatted there is nothing to undo. Reading Value while the user is typing returns the typed number without interrupting the edit. Change also fires as each character is typed; stepping at a limit, or committing text that parses to the unchanged value, does not fire it.

      +

      Its font must be fixed-pitch, every character the same width; the designer's Font dialog offers only those families for it. Given a proportional font it draws in the system font instead.

      Type-Specific Properties

        Property   Type      Description
         --------   -------   -------------------------------------------
      @@ -7110,6 +7353,7 @@ End

      TextBox

      VB Equivalent: TextBox -- DVX Widget: textbox (single-line text input, 256 chars unless MaxLength says otherwise)

      A single-line text input field. Supports data binding via DataSource and DataField properties.

      +

      Its font must be fixed-pitch, every character the same width; the designer's Font dialog offers only those families for it. Given a proportional font it draws in the system font instead.

      Type-Specific Properties

        Property       Type     Description
         ------------   ------   -------------------------------------------
      @@ -7121,7 +7365,7 @@ End
      DataField String Column name for data binding. SelStart Integer Where the selection starts, or the cursor when nothing is selected (0 = before the first character). Setting it moves the cursor there and selects nothing. SelLength Integer The number of characters selected (0 = none). Setting it selects that many characters from SelStart. - SelText String The selected text. Setting it replaces the selection (or inserts at the cursor), leaves the cursor after the new text and fires Change; in a masked box it fills the places from the selection's start, as pasting does. + SelText String The selected text. Setting it replaces the selection (or inserts at the cursor), leaves the cursor after the new text, and fires Change; in a masked box it fills the places from the selection's start, as pasting does.

      Type-Specific Methods

        Method      Description
         ------      -----------
      @@ -7130,7 +7374,7 @@ End
      Paste Put the clipboard's text in place of the selection (Ctrl+V; line breaks are left out); fires Change. SelectAll Select the whole text (Ctrl+A). Undo Take back the last change (Ctrl+Z); a second Undo puts it back. -

      The methods do what the keys do, so Cut, Paste and Undo do nothing in a read-only box. An Edit menu works on whichever box has the focus with Me.ActiveControl (see Form).

      +

      The methods do what the keys do, so Cut, Paste, and Undo do nothing in a read-only box. An Edit menu works on whichever box has the focus with Me.ActiveControl (see Form).

      Default Event: Change

      Example

      Begin TextBox Text1
      @@ -7147,6 +7391,7 @@ End Sub

      TextArea

      VB Equivalent: TextArea (DVX extension) -- DVX Widget: textarea (multi-line text input, default max 65536 chars)

      A multi-line text editing area. This is a DVX extension with no direct VB3 equivalent (VB uses a TextBox with MultiLine=True). Supports line numbers, auto-indent, find/replace, and tab configuration.

      +

      Its font must be fixed-pitch, every character the same width; the designer's Font dialog offers only those families for it. Given a proportional font it draws in the system font instead.

      Type-Specific Properties

        Property     Type      R/W   Description
         ----------   -------   ---   -------------------------------------------
      @@ -7156,7 +7401,7 @@ End Sub
      LineCount Integer R The number of lines (an empty text has one). SelStart Integer R/W Where the selection starts, or the cursor when nothing is selected (0 = before the first character). Setting it moves the cursor there and selects nothing. SelLength Integer R/W The number of characters selected (0 = none). Setting it selects that many characters from SelStart. - SelText String R/W The selected text. Setting it replaces the selection (or inserts at the cursor when nothing is selected), leaves the cursor after the new text and fires Change; Undo takes it back. + SelText String R/W The selected text. Setting it replaces the selection (or inserts at the cursor when nothing is selected), leaves the cursor after the new text, and fires Change; Undo takes it back. MaxLength Integer R/W The most characters the area holds (0 = the default, 65536; at most 262144). A smaller value cuts the text.

      Type-Specific Methods

        Method                                          Description
      @@ -7185,7 +7430,7 @@ End Sub
      SetTabWidth width% Set the tab stop width in characters. SetUseTabChar useChar When True, Tab inserts a tab character; when False, inserts spaces. Undo Take back the last change (Ctrl+Z); a second Undo puts it back. -

      Cut, Paste and Undo do nothing in a read-only area.

      +

      Cut, Paste, and Undo do nothing in a read-only area.

      Type-Specific Events

        Event         Parameters                Description
         -----------   -----------------------   -------------------------------------------
      diff --git a/docs/dvx_help_viewer.html b/docs/dvx_help_viewer.html
      index 1d524b56..9e713119 100644
      --- a/docs/dvx_help_viewer.html
      +++ b/docs/dvx_help_viewer.html
      @@ -59,7 +59,7 @@ img { max-width: 100%; }
       

      DVX Help Viewer

      DVX Help shows help pages. The list on the left is the table of contents, the page you are reading is on the right, and underlined words are links to other pages. You can also look things up in the Index or with Search.

      -
      Tip: New to DVX? Click the Program Manager window and press F1, or choose DVX Help from the Program Manager's Help menu, to open the DVX user guide. It explains how to start programs, work with windows, menus and dialog boxes, and everything else that works the same in every program. Each program's own commands are explained in that program's help: press F1 while you work in it.
      +
      Tip: New to DVX? Click the Program Manager window and press F1, or choose DVX Help from the Program Manager's Help menu, to open the DVX user guide. It explains how to start programs, work with windows, menus, and dialog boxes, and everything else that works the same in every program. Each program's own commands are explained in that program's help: press F1 while you work in it.

      Opening Help

      Press F1 from any DVX application to open context-sensitive help. Applications can register their own help file and topic so F1 opens the relevant page.

      If the help viewer is already running, F1 reuses its window: the requested page is shown there (loading the other application's help file if needed, which starts a fresh Back/Forward history) and the window is brought to the front. The Help menu of the Program Manager does the same. F1 pressed in the help viewer itself shows this help about the viewer. While the viewer's Index or Search dialog is open, the page asked for with F1 is shown once the dialog closes.

      @@ -97,7 +97,7 @@ img { max-width: 100%; } .link <id> <text> Hyperlink to another topic in the same help file (see Links below) .image <file> [left|center|right] - Inline image (BMP, PNG, JPEG or GIF), path + Inline image (BMP, PNG, JPEG, or GIF), path relative to the -i directory

      Block Directives

        .list                Start a bulleted list
      @@ -169,11 +169,11 @@ This is a warning message.
                             skipped)

      Multiple input files are merged into a single help file. This allows per-widget or per-feature documentation fragments to be combined automatically.

      Input Files

      -

      Each input file starts fresh: no topic is current and no .list, .note, .code or .table block is left open from the previous file. Text before a file's first .topic is ignored, and directives there are errors. A .code or .table block still open at the end of a file is closed with a warning. Files named with .include are different: they are read in place, as if their lines were part of the including file.

      +

      Each input file starts fresh: no topic is current and no .list, .note, .code, or .table block is left open from the previous file. Text before a file's first .topic is ignored, and directives there are errors. A .code or .table block still open at the end of a file is closed with a warning. Files named with .include are different: they are read in place, as if their lines were part of the including file.

      Source lines longer than 4094 characters, and lines that contain a NUL byte, are errors. The .image directive may name a path relative to the -i directory, including a subdirectory (for example sub/pic.png).

      If any error is reported the compile stops and the previous output file is kept.

      Links

      -

      Every .link must name a topic that is compiled into the same help file: a topic in any of the input files or the files they include, before or after the link. A link to any other ID is an error that names the file, the line and the missing ID. The one exception is the help DVX rebuilds by itself when it starts after programs, widgets or libraries were added or removed: there a link to a topic that is no longer present shows as plain text instead, so removing a piece of DVX never breaks the rest of the help. Links cannot reach into another help file; name the other file's page in the text instead. A .link line inside a .code block is example text and is never checked.

      +

      Every .link must name a topic that is compiled into the same help file: a topic in any of the input files or the files they include, before or after the link. A link to any other ID is an error that names the file, the line, and the missing ID. The one exception is the help DVX rebuilds by itself when it starts after programs, widgets, or libraries were added or removed: there a link to a topic that is no longer present shows as plain text instead, so removing a piece of DVX never breaks the rest of the help. Links cannot reach into another help file; name the other file's page in the text instead. A .link line inside a .code block is example text and is never checked.

      Build Integration

      The standard build pattern globs all fragments:

      dvxhlpc -o dvxhelp.hlp docs/src/overview.dhs widgets/*/*.dhs
      diff --git a/docs/dvx_system_reference.html b/docs/dvx_system_reference.html index 16f789ac..80711039 100644 --- a/docs/dvx_system_reference.html +++ b/docs/dvx_system_reference.html @@ -35,7 +35,7 @@ img { max-width: 100%; }
    • Working with Windows
    • Menus and Dialog Boxes
    • @@ -80,6 +80,8 @@ img { max-width: 100%; }
    • dvxTypes.h -- Shared Type Definitions
    • dvxCur.h -- Cursor Definitions
    • dvxFont.h -- Bitmap Font Data
    • +
    • dvxText.h -- Fonts
    • +
    • dvxFlow.h -- Flowing Text
    • dvxPal.h -- 8-bit Palette
    • dvxVideo.h -- Layer 1: Video Backend
    • dvxDraw.h -- Layer 2: Drawing Primitives
    • @@ -164,6 +166,11 @@ img { max-width: 100%; }
    • Music Library
    • Printing Library
    • +
    • Styled Text Library + +
    • Serial Stack
    • @@ -2012,7 +2103,7 @@ img { max-width: 100%; }

      Menus and Dialog Boxes

      Using Menus

      -

      Many windows have a menu bar under the title bar, with words such as File, Edit and Help. Each word opens a menu of commands.

      +

      Many windows have a menu bar under the title bar, with words such as File, Edit, and Help. Each word opens a menu of commands.

      With the mouse, click a menu name to open it, then click a command. While a menu is open, move the pointer along the menu bar to look at the other menus. Click anywhere outside the menu to close it without choosing anything.

      With the keyboard, press F10 to open the first menu, or hold Alt and press the underlined letter of a menu name. Then:

        @@ -2034,13 +2125,13 @@ img { max-width: 100%; }

        Many programs use the same menu names for the same kinds of commands:

          Menu   Usual commands
           ----   --------------
        -  File   New, Open..., Save, Save As..., Page Setup..., Print... and Exit.
        -  Edit   Cut, Copy, Paste and Select All.
        +  File   New, Open..., Save, Save As..., Page Setup..., Print..., and Exit.
        +  Edit   Cut, Copy, Paste, and Select All.
           Help   Contents opens the program's own help. About shows the program's name and copyright.

        Dialog Boxes

        A dialog box is a small window that asks you a question or asks for details. While a dialog box is open you must answer it before you can use the rest of the program.

          -
        • Enter presses the main button, usually OK, Open, Save or Yes.
        • +
        • Enter presses the main button, usually OK, Open, Save, or Yes.
        • Esc presses Cancel (or No), and closes the box without making changes.
        • Closing a dialog box with its Close box counts as Cancel.
        • Tab moves between the parts of the dialog box.
        • @@ -2053,11 +2144,11 @@ img { max-width: 100%; }
        • No throws the changes away.
        • Cancel goes back to your work without doing anything.
        -

        Next: Buttons, Lists and Other Controls

        +

        Next: Buttons, Lists, and Other Controls

        Opening and Saving Files

      -

      Buttons, Lists and Other Controls

      +

      Buttons, Lists, and Other Controls

      Windows and dialog boxes are built from a small set of controls. They all work the same way in every program.

      Buttons

      Click a button to use it. When a button is highlighted, Enter or the Space bar presses it too.

      @@ -2066,11 +2157,11 @@ img { max-width: 100%; }

      Option Buttons

      Option buttons come in groups, and only one in a group can be chosen. Click the one you want, or use the arrow keys to move through the group.

      Lists

      -

      A list shows several items. Click an item to select it, and double-click it (or press Enter) to open or use it. Up, Down, Page Up, Page Down, Home and End move through the list. Typing a letter jumps to the next item that starts with that letter.

      +

      A list shows several items. Click an item to select it, and double-click it (or press Enter) to open or use it. Up, Down, Page Up, Page Down, Home, and End move through the list. Typing a letter jumps to the next item that starts with that letter.

      Some lists let you select more than one item. Hold Ctrl while clicking to add or remove single items, or hold Shift while clicking to select everything between two items.

      Some lists have column headings. Click a heading to sort the list by that column; click it again to sort the other way. Drag the line between two headings to make a column wider or narrower.

      Drop-down Lists

      -

      A drop-down list shows one choice with an arrow button beside it. Click it to see all the choices, then click the one you want. With the keyboard, Up and Down change the choice, and Enter, the Space bar, F4 or Alt+Down opens the list. In an open list, press Enter, F4 or Alt+Down to take the highlighted choice.

      +

      A drop-down list shows one choice with an arrow button beside it. Click it to see all the choices, then click the one you want. With the keyboard, Up and Down change the choice, and Enter, the Space bar, F4, or Alt+Down opens the list. In an open list, press Enter, F4, or Alt+Down to take the highlighted choice.

      Some drop-down lists also let you type your own entry in the box.

      Text Boxes

      Click in a text box and type. These keys help you edit:

      @@ -2084,7 +2175,7 @@ img { max-width: 100%; } Ctrl+A Select everything. Ctrl+Z Undo the last change. Press it again to redo.

      You can also drag across text with the mouse to select it. Double-click selects a word, and a triple-click selects the whole line.

      -

      Cut, Copy and Paste work in every text box; see Copying and Pasting.

      +

      Cut, Copy, and Paste work in every text box; see Copying and Pasting.

      Copying and Pasting

      Number Boxes

      A number box has small up and down arrows. Click the arrows, press Up or Down, or type a number.

      @@ -2111,7 +2202,7 @@ img { max-width: 100%; }

      Opening a File

      Double-click a folder to look inside it. Click a file to put its name in the File name box, then click Open, or just double-click the file. If you type the name of a file that does not exist, the dialog asks whether you want to create it.

      Saving a File

      -

      Go to the folder you want, type a name in the File name box and click Save. If you leave off the ending (such as .txt), the dialog adds the one from the Filter box. If a file with that name already exists, DVX asks whether to replace it.

      +

      Go to the folder you want, type a name in the File name box, and click Save. If you leave off the ending (such as .txt), the dialog adds the one from the Filter box. If a file with that name already exists, DVX asks whether to replace it.

      Tip: Alt+D opens the Drive list from the keyboard. You can also change drives by typing the drive letter and a colon, such as A:, in the Path box and pressing Enter.
      @@ -2136,10 +2227,10 @@ img { max-width: 100%; }

      Printing

      Programs that can print have Print... in their File menu, and programs that print text also have Page Setup.... Every program prints to the same printer, so you only tell DVX once which printer you have and where it is plugged in (see Changing Settings). You can also send printouts into a file instead of a printer.

      The Print Dialog

      -

      File > Print... opens the Print dialog. Choose where the printout goes (a printer port such as LPT1, or a file), how many copies you want and, for a document of several pages, whether to print all of it or only some pages. Then choose Print.

      +

      File > Print... opens the Print dialog. Choose where the printout goes (a printer port such as LPT1, or a file), how many copies you want, and (for a document of several pages) whether to print all of it or only some pages. Then choose Print.

      While a printout is going to the printer, a small Printing window shows it is busy. You can keep working in other programs, and Cancel stops the printout.

      When the Printer Has a Problem

      -

      If the printer is out of paper, switched off, off line or not answering, a message says what is wrong. Fix the problem and choose Retry to carry on, or Cancel to stop printing. Nothing waits forever.

      +

      If the printer is out of paper, switched off, off line, or not answering, a message says what is wrong. Fix the problem and choose Retry to carry on, or Cancel to stop printing. Nothing waits forever.

      Page Setup

      File > Page Setup... chooses how text is laid out on paper: the margins, how many lines fit on a page, how wide a line is, and a header and footer printed on every page (for example the file name and the page number). Your choices are kept for next time and used by every program that prints text.

      Pictures

      @@ -2160,7 +2251,7 @@ img { max-width: 100%; }
      • Click anywhere in the program's window, or
      • Press Alt+Tab until its window comes to the front, or
      • -
      • Press Ctrl+Esc to open the Task Manager, pick the program and click Switch To.
      • +
      • Press Ctrl+Esc to open the Task Manager, pick the program, and click Switch To.

      Programs That Use the Whole Screen

      Some programs, games especially, can fill the whole screen instead of a window (many use Alt+Enter to switch). Your other programs keep running while they do. Alt+Tab and Ctrl+Esc still work and bring the desktop back; when you return to the program, it fills the screen again.

      @@ -2186,14 +2277,14 @@ img { max-width: 100%; }
    • The Search box finds programs in every group.
    • The status bar describes the selected program and says how many programs are running.
    -

    You can make your own groups, add, move and copy icons, give an icon a shortcut key that starts it from any program, and choose large icons, small icons or a list. Everything you arrange is remembered the next time you start DVX. The Program Manager's own help explains all of this: press F1 while it is the active window, or choose Contents from its Help menu.

    +

    You can make your own groups, add, move, and copy icons, give an icon a shortcut key that starts it from any program, and choose large icons, small icons, or a list. Everything you arrange is remembered the next time you start DVX. The Program Manager's own help explains all of this: press F1 while it is the active window, or choose Contents from its Help menu.

    Warning: Closing the Program Manager window leaves DVX, after asking you first.

    Changing Settings

    -

    The mouse, the screen colors, the desktop picture, the screen resolution, which program opens each kind of document, the network, the printer, joysticks and the sounds are changed with the settings program, which has an icon in the Installed Programs group of the Program Manager. Start it like any other program. Its own help, opened with F1 or its Help menu, explains every setting.

    +

    The mouse, the screen colors, the desktop picture, the screen resolution, which program opens each kind of document, the network, the printer, joysticks, and the sounds are changed with the settings program, which has an icon in the Installed Programs group of the Program Manager. Start it like any other program. Its own help, opened with F1 or its Help menu, explains every setting.

    Settings you keep are saved, and DVX uses them again the next time it starts.

    -

    DVX plays a short sound when it starts and when it ends, and when a message appears (a different one for errors, warnings, questions and information). Each sound can be changed or turned off, and the volume set, in the settings program.

    +

    DVX plays a short sound when it starts and when it ends, and when a message appears (a different one for errors, warnings, questions, and information). Each sound can be changed or turned off, and the volume set, in the settings program.

    Tip: If a new screen resolution leaves the screen unreadable, just wait: DVX goes back to the old one after 15 seconds unless you confirm the change.
    @@ -2275,12 +2366,13 @@ img { max-width: 100%; }

    If DVX starts but the screen cannot be read (the monitor cannot show the saved resolution), hold down a Shift key while DVX starts. DVX then starts in safe video mode and asks the same question. Typing DVX /SAFE at the DOS prompt does the same.

    You can pick another resolution at any time with the settings program (see Changing Settings).

    There is no sound

    -

    DVX finds the sound hardware by itself. A Sound Blaster needs its BLASTER setting in AUTOEXEC.BAT, for example SET BLASTER=A220 I5 D1 H5 (the card's manual gives the numbers). Without a sound card DVX uses the PC speaker, which plays tones and beeps but not recorded sounds. The settings program shows the sound hardware in use, lets you pick it yourself and sets the volume; if a sound is set to none there, nothing is heard for it. Without any sound hardware DVX and its programs work the same, silently.

    +

    DVX finds the sound hardware by itself. A Sound Blaster needs its BLASTER setting in AUTOEXEC.BAT, for example SET BLASTER=A220 I5 D1 H5 (the card's manual gives the numbers). Without a sound card DVX uses the PC speaker, which plays tones and beeps but not recorded sounds. The settings program shows the sound hardware in use, lets you pick it yourself, and sets the volume; if a sound is set to none there, nothing is heard for it. Without any sound hardware DVX and its programs work the same, silently.

    An empty floppy or CD-ROM drive

    When a program tries to read a drive that has no disk in it, or to write to a write-protected disk, the program simply reports that it could not read or write the file. DVX never stops at the DOS "Abort, Retry, Fail" question. Put the disk in and try again.

    -

    Installing, upgrading and removing DVX

    -

    DVX is installed with SETUP, from the DVX CD or from Disk 1 of the DVX floppy disks: change to that drive and type SETUP. Setup shows the licence, asks for the folder to install DVX in, checks that there is room and unpacks DVX there, asking for each floppy disk in turn. It can also make DVX start when the computer starts (it shows the line it adds to AUTOEXEC.BAT and keeps a copy of the old file as AUTOEXEC.DVX). Setup never writes to the CD or the floppy disks.

    -

    To upgrade or repair DVX, run Setup again and choose the same folder: answer Yes to keep your settings. To remove DVX, run SETUP in the DVX folder (for example C:\DVX\SETUP) and choose Uninstall. Files you added to the folder yourself are not removed, and your settings are only removed if you ask.

    +

    Installing, upgrading, and removing DVX

    +

    DVX is installed with SETUP, from the DVX CD or from Disk 1 of the DVX floppy disks: change to that drive and type SETUP. Setup shows the licence, asks for the folder to install DVX in and which fonts to install, checks that there is room, and unpacks DVX there, asking for each floppy disk it needs in turn. It can also make DVX start when the computer starts (it shows the line it adds to AUTOEXEC.BAT and keeps a copy of the old file as AUTOEXEC.DVX). Setup never writes to the CD or the floppy disks.

    +

    The fonts are up to you. None keeps DVX to its own font; the base set adds DejaVu Sans, Sans Mono, and Serif in all their styles (about 2 MB); and All adds every font that comes with DVX (about 8 MB). A program that asks for a font you did not install shows its text in DVX's own font instead, unless it carries that font itself (some programs do, such as Welcome and the DVX Demo). Every font file holds its own licence: the Licence button of the Font dialog shows it. With None or the base set, Setup does not ask for the last floppy disks, which hold only fonts.

    +

    To upgrade or repair DVX, run Setup again and choose the same folder: answer Yes to keep your settings. Setup starts with the fonts that folder already has; choose others to add fonts or remove them. To remove DVX, run SETUP in the DVX folder (for example C:\DVX\SETUP) and choose Uninstall. Files you added to the folder yourself are not removed, and your settings are only removed if you ask.

    Running DVX from a CD-ROM or a write-protected disk

    DVX also runs straight from a disk it cannot write to. It then tells you once that changed settings last only until you exit DVX, and the help files are used as they are. To keep your settings, install DVX on your hard disk with Setup.

    A program is already running

    @@ -2302,7 +2394,7 @@ img { max-width: 100%; }

    For Developers

    -

    The rest of this help file is a technical reference for programmers who write programs, controls or add-ons for DVX in C. It is listed under Technical Reference (for Developers) in the table of contents.

    +

    The rest of this help file is a technical reference for programmers who write programs, controls, or add-ons for DVX in C. It is listed under Technical Reference (for Developers) in the table of contents.

    Start with the Developer Overview

    Programs can also be written in BASIC, if the BASIC programming environment is installed. It is easier to learn and has its own help: open it and press F1.

    @@ -2347,7 +2439,7 @@ img { max-width: 100%; }
  • Every header uses stdint.h types (int32_t, uint8_t) and stdbool.h types (bool).
  • Getting Started

    -

    To write a program, widget or library right away, start with the SDK Quick Start: it creates a working project from a template and walks through building, running, debugging and installing it.

    +

    To write a program, widget, or library right away, start with the SDK Quick Start: it creates a working project from a template and walks through building, running, debugging, and installing it.

    SDK Quick Start

    To understand how DVX works, read these topics in order:

      @@ -2597,7 +2689,7 @@ img { max-width: 100%; } dvxMessageBox Generic message box with button/icon flags (MB_OK, MB_YESNO, MB_ICONERROR, etc.) dvxErrorBox Shortcut for MB_OK + MB_ICONERROR dvxInfoBox Shortcut for MB_OK + MB_ICONINFO - dvxFileDialog File open/save dialog with directory navigation, a drive dropdown and a filter dropdown + dvxFileDialog File open/save dialog with directory navigation, a drive dropdown, and a filter dropdown dvxInputBox Single-line text input with OK/Cancel dvxIntInputBox Integer input with spinner, clamp range, and step dvxChoiceDialog List selection with OK/Cancel @@ -2621,7 +2713,8 @@ img { max-width: 100%; } ------- ----- ------- DVX_RES_ICON 1 Image data (BMP or PNG, typically 16x16 or 32x32 icons) DVX_RES_TEXT 2 Null-terminated string (localisable UI text, author, copyright) - DVX_RES_BINARY 3 Arbitrary data (app-specific) + DVX_RES_BINARY 3 Arbitrary data (app-specific) + DVX_RES_FONT 4 A .DFN font the program carries (the shell adds it for the program when it loads it)

      Runtime API

        Function           Description
         --------           -----------
      @@ -2710,7 +2803,7 @@ prefsClose(h);
    • Poll mouse -- platformMousePoll() returns position and button bitmask. Compare with previous frame for press/release edge detection.
    • Poll keyboard -- platformKeyboardRead() returns ASCII + scancode. Non-blocking; returns false if buffer is empty. Shift/Ctrl/Alt+F-keys arrive as the plain F-key plus modifier flags. If an input entropy hook is installed (dvxSetInputEntropyHook), input events are timestamped and fed to it here.
    • Dispatch to focused window -- the event loop fires window callbacks (onKey, onMouse, etc.) on the focused window. If the window has a widget tree, the widget system's installed handlers dispatch to individual widgets.
    • -
    • Tooltips, widget polling (WCLASS_NEEDS_POLL), cursor blink and widget timers.
    • +
    • Tooltips, widget polling (WCLASS_NEEDS_POLL), cursor blink, and widget timers.
    • Deferred paint flush -- every window whose paintNeeded was raised by wgtInvalidate / wgtInvalidatePaint during the frame is painted once (the highest requested level wins). Windows queued for destruction are skipped.
    • Deferred destroy flush -- windows destroyed from inside callbacks are torn down once the dispatch depth is back to zero.
    • Compositor pass -- merge dirty rects, composite, flush to LFB.
    • @@ -2736,7 +2829,7 @@ prefsClose(h); wclsOnMouse / wclsOnKey (vtable dispatch) | Universal callbacks (onClick, onChange, etc.) -

      Mouse events use two hit tests. widgetHitTest finds the widget whose class handles the press; it stops at a WCLASS_NO_HIT_RECURSE container (Splitter, ScrollPane), so the container's ON_MOUSE handler still gets the press (divider drag, scrollbar clicks, forwarding into its children). widgetCallbackTarget continues through non-focusable NO_HIT_RECURSE containers into the child whose paintClip (the visible part when it was last painted) contains the point. The universal callbacks (onClick, onDblClick, onMouseDown/Up/Move), context menus, tooltips and the cursor shape use that target, so a control inside a Splitter pane or ScrollPane viewport gets its own events only where it is visible; on the container's own chrome the container is the target.

      +

      Mouse events use two hit tests. widgetHitTest finds the widget whose class handles the press; it stops at a WCLASS_NO_HIT_RECURSE container (Splitter, ScrollPane), so the container's ON_MOUSE handler still gets the press (divider drag, scrollbar clicks, forwarding into its children). widgetCallbackTarget continues through non-focusable NO_HIT_RECURSE containers into the child whose paintClip (the visible part when it was last painted) contains the point. The universal callbacks (onClick, onDblClick, onMouseDown/Up/Move), context menus, tooltips, and the cursor shape use that target, so a control inside a Splitter pane or ScrollPane viewport gets its own events only where it is visible; on the container's own chrome the container is the target.

      Accelerator Tables

      Per-window accelerator tables map key + modifier combinations to command IDs. The runtime normalizes key/modifier at registration time with the same rule it applies to every keystroke (letters uppercased, Ctrl+letter and Alt+letter mapped to the letter, Ctrl/Alt always significant, Shift significant except for a printable character typed without Ctrl/Alt), so matching at dispatch time is two integer comparisons per entry. Matched accelerators fire the window's onMenu callback with the command ID, unifying the menu and hotkey code paths.

      Mouse Wheel

      @@ -2748,28 +2841,70 @@ prefsClose(h);

    Font System

    -

    DVX uses fixed-width 8-pixel-wide bitmap fonts only. One size is provided: 8x16, matching the standard VGA ROM font and CP437 encoding (256 glyphs).

    +

    Every text call takes a BitmapFontT, and there are two kinds of them. The system font is the 8x16 VGA ROM font (code page 437, 256 glyphs) built into libdvx: 8-pixel cells, one byte per glyph row, so character positions are a multiplication, glyph lookup an index, and every scanline a single byte -- the hot paths of drawChar, drawTextN, and drawTermRow depend on it. A loaded font (dvxText.h) is one size of a font family from the FONTS folder (Setup installs none, the base set of DejaVu Sans, Sans Mono, and Serif, or all of them; a family that is not there opens as the system font): per-glyph advances and bitmap boxes, any width, proportional or fixed-pitch, and in up to four styles. The same calls draw and measure both; a loaded font's glyphs field is what sends them to the general paths.

    BitmapFontT

      typedef struct {
    -      int32_t        charWidth;    // fixed width per glyph (always 8)
    -      int32_t        charHeight;   // 16
    -      int32_t        firstChar;    // typically 0
    -      int32_t        numChars;     // typically 256
    -      const uint8_t *glyphData;    // packed 1bpp, charHeight bytes per glyph
    +      int32_t           charWidth;    // cell width (fixed pitch) or the digit 0's advance
    +      int32_t           charHeight;   // line height
    +      int32_t           firstChar;    // 0
    +      int32_t           numChars;     // 256
    +      const uint8_t    *glyphData;    // system font: charHeight bytes per glyph; loaded: bitmaps
    +      const FontGlyphT *glyphs;       // loaded font: per-glyph metrics; NULL: the system font
    +      const uint32_t   *glyphAt;      // loaded font: each glyph's bitmap offset
    +      int32_t           ascent;       // top of a line to the baseline
    +      int32_t           pixelSize;
    +      uint32_t          style;        // FONT_STYLE_BOLD / ITALIC
    +      uint32_t          flags;        // FONT_FLAG_PROPORTIONAL, _SYNTH_BOLD, _SYNTH_ITALIC, _GRAY
    +      int32_t           underlineY;   // underline and accelerator row
    +      int32_t           strikeY;      // strike-through row
    +      int32_t           lineThick;
    +      void             *priv;         // the font cache's entry
    +      uint32_t          serial;       // new whenever the glyphs are built (again)
       } BitmapFontT;
    -

    Design rationale:

    -
      -
    • Character positions are pure multiplication (x = col * 8).
    • -
    • Glyph lookup is a single array index.
    • -
    • Each scanline of a glyph is exactly one byte (1bpp at 8 pixels wide).
    • -
    • No glyph-width tables, kerning, or per-character positioning needed.
    • -
    • 8-pixel width aligns with byte boundaries -- no bit shifting in per-scanline rendering.
    • -
    +

    A FontGlyphT is six bytes: advance, bitmap width and height, the bitmap's offset from the pen (left, and top up from the baseline), and flags. A glyph's bitmap is height rows of (width + 7) / 8 bytes, the leftmost pixel in the top bit.

    Text Rendering Functions

    drawChar() -- Renders a single character. Supports opaque (background fill) and transparent modes.

    -

    drawTextN() -- Optimized batch rendering for a known character count. Clips once for the entire run, fills background in a single rectFill, then overlays glyph foreground pixels. Significantly faster than per-character rendering for long runs.

    +

    drawTextN() -- Optimized batch rendering for a known character count. Clips once for the entire run, fills background in a single rectFill, then overlays glyph foreground pixels. Significantly faster than per-character rendering for long runs. In a loaded font each glyph is drawn at the pen; every bitmap byte whose 8 pixels lie inside the clip rectangle goes through the same unrolled row writers as the system font, so only the bytes on a clip edge are drawn pixel by pixel.

    drawTermRow() -- Renders an 80-column terminal row in a single pass, with per-cell foreground/background from a 16-color palette, blink attribute support, and cursor rendering. Exists because per-character terminal rendering is unacceptably slow on target hardware.

    -

    drawTextAccel() -- Renders text with & accelerator markers. The character after & is underlined to indicate the keyboard shortcut.

    +

    drawTextAccel() -- Renders text with & accelerator markers. The character after & is marked with the font's underline across its advance to indicate the keyboard shortcut.

    +

    drawTextStyled() -- Text with an underline and / or strike-through as wide as it, at the font's rows.

    +

    textWidth(), textWidthN(), textWidthAccel() -- Widths: for the system font a multiplication, for a loaded font the sum of the advances.

    +

    Loading Fonts

    +

    dvxFontOpen(family, size, style) reads the family list from FONTS (once, at the first call: each .DFN file's header and size table), picks the face for the style -- the exact one, else one with fewer style bits whose missing bits are made -- and that face's size nearest the one asked for. It never scales. The font is built once into a glyph table and bitmaps of its own, so drawing never decides anything per glyph:

    +
      +
    • A character the face has is copied; when a style is made, bold thickens each stroke by a column (two from 24 pixels) and italic moves each row one column right per four rows above the baseline.
    • +
    • Box drawing, block, and shade characters (CP437 B0-DF) the face lacks -- or all of them, when its file asks, or when a made bold widens a fixed pitch -- are drawn to fill the character cell from a table of each character's arms (none, single, or double; up, down, left, and right), so lines join across characters. They are never thickened or slanted.
    • +
    • Any other character the face lacks comes from DejaVu Sans (DejaVu Sans Mono for a fixed-pitch face, centered in its pitch) at the nearest size in the same style, or from the system font when those are not installed.
    • +
    • What nothing has is a blank as wide as a space.
    • +
    +

    Fonts are cached and counted: the same family, size, and style opened again is the same font. Up to eight fonts no one holds stay loaded, the least recently used unloaded first. Everything the font system allocates is charged to the system, not to the program that happened to open a font.

    +

    Fonts a Program Carries

    +

    A family is installed (the FONTS folder, read by its full path so a program that changes the current folder still finds them) or added for an owner: a program's app id, or 0 for every program. The shell calls dvxFontAddResources for each program it loads (its DVX_RES_FONT resources, read in place: a font source is a file and the font's offset and length in it) and dvxFontRemoveOwner when its resources are released; a program can add .DFN files or fonts in memory itself. Every lookup is for the running program (*dvxMemAppIdPtr, the shell's currentAppId; window callbacks run as the window's program).

    +

    Opening a font picks its face per style from the program's own family and the installed one together: the style's faces in sFaceOrder's order, the first style either has; then the nearest size of either face's sizes; then the program's face when it has that size, else the installed one. So a program's faces and sizes win, and the installed family fills in the rest. A program with no families of its own shares the cache with every other; one with its own gets fonts built for it alone (their fallback glyphs may come from its families too). A program's fonts that change or go are detached from the cache: the ones no one holds unloaded, held ones kept as they are.

    +

    Each face holds its source counted, and each font built from it holds it too, so a font's licence (dvxFontLicence) is always the one of the face it was built from, its program ended or not.

    +

    The building works on one level per pixel (0 to FONT_GRAY_FULL) whatever the source, and packs the finished glyph at the font's own depth, so made styles, box characters, and fallback glyphs are the same code for both kinds of font.

    +

    Smoothed Text

    +

    A .DFN size can come twice: 1-bit, and smoothed -- FONT_GRAY_BPP (2) bits per pixel, four levels from none to full, rendered by FreeType with light hinting. mkfont smooths the sizes fonts.ini lists under smooth. A 1-bit size is FreeType's 1-bit hinted rendering, except at the sizes listed under graycut, where it is the light-hinted rendering with every pixel at least half covered set: some faces' 1-bit hinting draws them too light (DejaVu Serif Bold at its smoothed sizes, which comes out near regular weight, and DejaVu Sans Bold at 8 pixels). When smoothing is on (dvxFontSetSmooth), such a size opens from its smoothed strike and the font has FONT_FLAG_GRAY; every other size, and every face without one, stays 1-bit.

    +

    Drawing a full-level pixel is a store of the text color, as for a 1-bit font. A partial one is the text color blended into the pixel under it, channel by channel through the display's PixelFormatT masks and shifts; text mostly stands on one color, so each glyph keeps its last blend per level and reuses it while the color under it stays the same. Only partial pixels cost more than 1-bit text. At 8 bits per pixel there is nothing to blend with: the pixels of FONT_GRAY_HALF and more are drawn and the rest dropped. The scaled text path (drawTextScaled) does the same.

    +

    The application decides (dvxSetFontSmoothing): smoothing is on when the user wants it (DVX.INI [fonts] smoothing, on by default; Control Panel's Video page) and the screen has more than 8 bits per pixel, and a video mode change applies it again. Turning it on or off builds every open font again in place: widgets keep their font pointers, the glyphs change under them, and the font's serial changes so caches of measurements (list column widths and the like) know to measure again. Every window is then laid out and painted again.

    +

    Widget Fonts

    +

    Any widget can have a font of its own (wgtSetFont; BASIC's FontName, FontSize, FontBold ... properties), and the widgets inside a container draw with the container's font unless they have their own, so a form's font is the default for everything on it. wgtFontFor(w, inherited) walks up the parent chain to the nearest font set, else takes the one passed down (the application's). The class dispatch helpers hand every paint, measure, and layout method that font, so widget code just uses its font parameter -- the same code serves the system font, a proportional font, and a fixed-pitch one of any size. Event handlers, which are given no font, call wgtFont(w). Widgets measure text with textWidth / textWidthN, never with character counts. A class that edits text by columns (TextBox, Terminal, ComboBox, Spinner: WCLASS_FIXED_FONT) given a proportional font draws with the system font instead; a fixed-pitch font of any size works there, its column math using that font's charWidth and charHeight.

    +

    Underline and strike-through are part of a font, as in Visual Basic: a font opened with them shares the glyphs of the same font without them, and every text call draws its lines. A widget holds its font open while it is set and closes it when destroyed (widgetFree, the one place a widget is freed).

    +

    Rich Text

    +

    Text in several fonts in one control is built on the text flow (dvxFlow.h): runs of text, each in a font and a color, laid out into wrapped, aligned lines as tall as the tallest font on them, with offsets for carets, hit tests, and selections. The RichLabel widget is one flow built by calls. The RichEdit widget keeps its document in the richtext library (styled text: runs of interned styles over code page 437 text, an alignment per paragraph, copy / replace / restyle, and RTF in and out) and lays each paragraph out as a flow of its own, so an edit lays out only the paragraphs it touched and repaints only the rows they hold (its quick repaint); a change of height repaints the widget. Its undo steps are pieces of styled text swapped back in, and Copy keeps a styled copy for a paste of the same text.

    +

    Font Files (.DFN)

    +

    One file holds one family in one style at every size it comes in, made on the build host by mkfont (src/tools/mkfont.c) with FreeType from TrueType, OpenType, Type 1, BDF, and PCF fonts, as listed in src/fonts/fonts.ini. A .DFN is a pure format conversion: the converter never adds or changes a glyph, it only marks the characters a face lacks. Little-endian, every record read whole:

    +
      Part                      Size            Holds
    +  ----                      ----            -----
    +  DfnHeaderT                60 bytes        "DVXF", format version (2), style, face flags (fixed pitch; box characters drawn by DVX), strike count (1 to 32), family name (31 characters), where the notice and licence texts are and their lengths
    +  DfnStrikeT x count        24 bytes each   per size, smallest first: where its glyph table and bitmaps are, the bitmaps' size, the pixel size, ascent, descent, line height, widest advance
    +  FontGlyphT x 256          6 bytes each    per size, at its glyph table offset
    +  glyph bitmaps             as listed       per size, back to back in character order
    +  notice text               as listed       the family and style, the source fonts it was converted from, and their copyright notices
    +  licence text              as listed       the licence of the font it came from
    +

    Each glyph's bitmap starts where the one before ends, so the reader (dvxFontFmt.c) works out the offsets and checks that they fill the strike's bitmaps exactly. Every count, offset, and size is checked against the font's length before it is used: a damaged file is refused, never read past.

    +

    Each file carries what its licence asks to go with every copy of the font: the notice and licence texts, transliterated to ASCII with CR LF line ends. They are read only when asked for (dvxFontLicence, the Font dialog's Licence button), never when text is drawn, so they cost nothing at run time. A font goes nowhere without its licence: copied, installed, or carried inside a program as a resource.

    +

    The reader takes a DfnSourceT: a file of its own, a font inside another file at an offset (a program's font resource), or a font in memory. dfnOpen reads the header and strike table, dfnLoadStrike one size, and dfnLoadText a text.

    Performance Optimization

    AppContextT stores a fixed-point 16.16 reciprocal of font.charHeight (charHeightRecip) so that dividing by charHeight (the pixel-to-row mapping for popup menu and system menu hover/click) becomes a multiply+shift instead of an integer divide, which costs 40+ cycles on a 486.

    @@ -2822,7 +2957,7 @@ prefsClose(h);

    Optimized Memory Spans

    Six functions: platformSpanFill8/16/32() and platformSpanCopy8/16/32(). Called once per scanline of every rectangle fill, blit, and text draw. On DOS these use inline assembly for critical inner loops.

    Mouse Input

    -

    Polling model. platformMousePoll() returns position and button bitmask. Wheel support via CuteMouse API. platformMouseInit() can be called again after a video-mode change; it re-applies the speed, acceleration and wheel settings after the driver reset.

    +

    Polling model. platformMousePoll() returns position and button bitmask. Wheel support via CuteMouse API. platformMouseInit() can be called again after a video-mode change; it re-applies the speed, acceleration, and wheel settings after the driver reset.

    Keyboard Input

    platformKeyboardRead() -- non-blocking key read; Shift/Ctrl/Alt+F1..F12 are reported as the plain F-key scancode (platformNormalizeFKeyScan). The keyboard hook (a chain on INT 9 on DOS; its code and data are pinned with platformLockRegion) decodes every key going down and up into a held-key table and a queue of raw events (platformKeyEventRead, platformKeyIsDown), which feed onKeyUp and game input windows. platformAltScanToChar() -- scancode-to-ASCII lookup for Alt+key combinations.

    Timing

    @@ -2923,6 +3058,8 @@ prefsClose(h);
  • dvxTypes.h -- Shared type definitions and constants
  • dvxCur.h -- Embedded mouse cursor bitmaps
  • dvxFont.h -- Embedded 8x16 CP437 bitmap font
  • +
  • dvxText.h -- Font families and loaded fonts
  • +
  • dvxFlow.h -- Flowing text: runs in fonts and colors, wrapped and aligned
  • dvxPal.h -- 8-bit mode 256-color palette
  • dvxVideo.h -- Layer 1: VESA VBE video backend
  • dvxDraw.h -- Layer 2: 2D drawing primitives
  • @@ -2941,6 +3078,8 @@ prefsClose(h);

    dvxTypes.h -- Shared Type Definitions

    dvxCur.h -- Cursor Definitions

    dvxFont.h -- Bitmap Font Data

    +

    dvxText.h -- Fonts

    +

    dvxFlow.h -- Flowing Text

    dvxPal.h -- 8-bit Palette

    dvxVideo.h -- Layer 1: Video Backend

    dvxDraw.h -- Layer 2: Drawing Primitives

    @@ -2960,7 +3099,7 @@ prefsClose(h);

    dvxTypes.h -- Shared Type Definitions

    Central type definitions shared across all five layers of the DVX GUI stack. Every header includes this file. Contains no function definitions -- only structs, enums, typedefs, and compile-time constants.

    Binary Interface Version

    -

    DVX_ABI_VERSION is one number for the whole binary interface between dvx.exe, libraries (.lib), widgets (.wgt) and apps (.app). Every module records the value it was compiled with, and a module whose value differs from the running system's is refused instead of being allowed to call the wrong functions:

    +

    DVX_ABI_VERSION is one number for the whole binary interface between dvx.exe, libraries (.lib), widgets (.wgt), and apps (.app). Every module records the value it was compiled with, and a module whose value differs from the running system's is refused instead of being allowed to call the wrong functions:

      Module / table   Where the value is recorded                                    Checked by
       --------------   ---------------------------                                    ----------
       .lib, .wgt       DVX_ABI_STAMP(baseName) exports <baseName>AbiVersion           The loader, right after loading the module. A missing or different stamp stops the boot with a message naming the file (details in DVX.LOG).
    @@ -2972,7 +3111,7 @@ prefsClose(h);
    #include "dvxTypes.h"
     
     DVX_ABI_STAMP(mylib);
    -

    Bump rule: increment DVX_ABI_VERSION in the same change that alters anything an already compiled module depends on -- the size, member order or member types of a public struct or widget API struct in an SDK header; a WGT_METHOD_* slot or the meaning of a WidgetClassT slot; the parameters, return type or calling contract of a public or exported function (or its removal); the value of a public constant modules compile in. Adding a function, a constant, or a struct no existing struct embeds needs no bump. After a bump, run make -C src/test/abi update to regenerate the committed size table (src/test/abi/abiSizes.txt). The abi host test fails when a public struct changes size without a bump, or when the table was not regenerated.

    +

    Bump rule: increment DVX_ABI_VERSION in the same change that alters anything an already compiled module depends on -- the size, member order, or member types of a public struct or widget API struct in an SDK header; a WGT_METHOD_* slot or the meaning of a WidgetClassT slot; the parameters, return type, or calling contract of a public or exported function (or its removal); the value of a public constant modules compile in. Adding a function, a constant, or a struct no existing struct embeds needs no bump. After a bump, run make -C src/test/abi update to regenerate the committed size table (src/test/abi/abiSizes.txt). The abi host test fails when a public struct changes size without a bump, or when the table was not regenerated.

    Core Structures

    PixelFormatT

    Describes the pixel encoding for the active VESA video mode. Populated once at startup from the VBE mode info block, then treated as read-only.

    @@ -3017,14 +3156,47 @@ DVX_ABI_STAMP(mylib); uint32_t face Interior fill color (0 = no fill) int32_t width Border thickness in pixels (typically 2)

    BitmapFontT

    -

    Fixed-width 8-pixel-wide bitmap font descriptor. One size provided: 8x16 (standard VGA ROM font, CP437 encoding).

    +

    The one font type every text call takes. Two kinds share it: the built-in system font (dvxFont8x16: 8-pixel cells of the VGA 8x16 font, one byte per glyph row, glyphs NULL; text takes the byte-aligned fast paths) and a loaded font (dvxFontOpen, dvxText.h: a size of a .DFN font file, or the system font made bold or italic), whose glyphs give each character's advance and bitmap box. Every character of code page 437 draws in either kind.

      Field                          Description
       -----                          -----------
    -  int32_t charWidth              Fixed width per glyph (always 8)
    -  int32_t charHeight             Glyph height (16 in the bundled font)
    -  int32_t firstChar              ASCII code of first glyph (typically 0)
    -  int32_t numChars               Number of glyphs (typically 256)
    -  const uint8_t *glyphData       Packed 1bpp data, charHeight bytes per glyph
    + int32_t charWidth Cell width of a fixed-pitch font; the digit 0's width in a proportional one (for "so many characters wide") + int32_t charHeight Line height + int32_t firstChar Code of the first glyph (0) + int32_t numChars Number of glyphs (256) + const uint8_t *glyphData System font: charHeight bytes per glyph; loaded font: the glyph bitmaps + const FontGlyphT *glyphs Loaded font: each character's glyph; NULL for the system font + const uint32_t *glyphAt Loaded font: where each glyph's bitmap starts in glyphData + int32_t ascent Rows from the top of a line to the baseline + int32_t pixelSize The size the font was made at (16 for the system font) + uint32_t style FONT_STYLE_BOLD / FONT_STYLE_ITALIC the font draws + uint32_t flags FONT_FLAG_PROPORTIONAL, FONT_FLAG_SYNTH_BOLD, FONT_FLAG_SYNTH_ITALIC, FONT_FLAG_GRAY + int32_t underlineY Row of an underline and of accelerator marks, from the top of a line + int32_t strikeY Row of a strike-through line + int32_t lineThick Rows in those lines + void *priv The font cache's (dvxFontClose); NULL for fonts not from dvxFontOpen + uint32_t serial A loaded font's own number, never reused; new when its glyphs are built again (dvxFontSetSmooth). 0 for the system font as compiled in +

    Measure text with textWidth / textWidthN, never strlen * charWidth: a loaded font may be proportional.

    +

    FontGlyphT

    +

    One character of a loaded font, the same 6-byte record a .DFN file stores. The glyph's bitmap is height rows of FONT_ROW_BYTES(width) bytes, the leftmost pixel in the top bit, at glyphData + glyphAt[character]. In a smoothed font (FONT_FLAG_GRAY) each pixel is FONT_GRAY_BPP bits instead: rows of FONT_ROW_BYTES_BPP(width, FONT_GRAY_BPP) bytes, a pixel's level 0 (none) to FONT_GRAY_FULL read with FONT_GRAY_LEVEL(row, x); FONT_BPP(font) is a font's bits per pixel.

    +
      Field                          Description
    +  -----                          -----------
    +  uint8_t advance                Pen movement after the glyph
    +  uint8_t width, height          Bitmap size
    +  int8_t left                    Pen position to the bitmap's left column (negative: the glyph reaches back)
    +  int8_t top                     Baseline to the bitmap's top row (up is positive)
    +  uint8_t flags                  FONT_GLYPH_MISSING in a file (a loaded font has every glyph)
    +

    Font styles and flags

    +
      Define                   Value   Description
    +  ------                   -----   -----------
    +  FONT_STYLE_BOLD          0x01    Bold face (dvxFontOpen)
    +  FONT_STYLE_ITALIC        0x02    Italic face (dvxFontOpen)
    +  FONT_STYLE_UNDERLINE     0x04    Underline drawn with the text (drawTextStyled)
    +  FONT_STYLE_STRIKE        0x08    Strike-through drawn with the text (drawTextStyled)
    +  FONT_STYLE_FACE          0x03    The face bits (bold and italic)
    +  FONT_FLAG_PROPORTIONAL   0x01    Characters differ in width
    +  FONT_FLAG_SYNTH_BOLD     0x02    Bold made from the regular face (thickened)
    +  FONT_FLAG_SYNTH_ITALIC   0x04    Italic made by slanting the upright face
    +  FONT_FLAG_GRAY           0x08    Smoothed: glyph pixels of FONT_GRAY_BPP bits (dvxFontSetSmooth)

    ColorSchemeT

    All UI colors pre-packed into display pixel format at init time. Theme support is achieved by swapping this struct.

      Field                                              Description
    @@ -3091,7 +3263,7 @@ DVX_ABI_STAMP(mylib);
    onFocus(WindowT *win) Window gained focus onBlur(WindowT *win) Window lost focus onRawKey(WindowT *win, const RawKeyEventT *evt) Key pressed or released (gameInput windows) -

    Game input: while a window with gameInput set has the keyboard focus, DVX keeps only Alt+Tab, Ctrl+Esc and Alt+F4 for itself. Every other keystroke, F1, F10, Alt+letter, Ctrl+F12, accelerators and hotkeys included, goes straight to onKey (except while a menu opened with the mouse, or the keyboard move/size mode, is taking keys). onRawKey then receives each key going down and up (RawKeyEventT: set-1 scancode, extended flag, repeat flag, modifiers from the held-key table), after the frame's onKey calls. When the focus moves away the window gets a release for every key still held (with no modifiers), and the held-key table starts over for the next window. onKeyUp keeps working as for any window. Mouse input is unchanged. The game library (game.h) adds gameKeyIsDown, frame pacing and timers.

    +

    Game input: while a window with gameInput set has the keyboard focus, DVX keeps only Alt+Tab, Ctrl+Esc, and Alt+F4 for itself. Every other keystroke, F1, F10, Alt+letter, Ctrl+F12, accelerators, and hotkeys included, goes straight to onKey (except while a menu opened with the mouse, or the keyboard move/size mode, is taking keys). onRawKey then receives each key going down and up (RawKeyEventT: set-1 scancode, extended flag, repeat flag, modifiers from the held-key table), after the frame's onKey calls. When the focus moves away the window gets a release for every key still held (with no modifiers), and the held-key table starts over for the next window. onKeyUp keeps working as for any window. Mouse input is unchanged. The game library (game.h) adds gameKeyIsDown, frame pacing, and timers.

    WindowStackT

    Z-ordered window stack (front-to-back: index count-1 is topmost). Owns system-wide drag/resize/scroll interaction state.

      Field                                              Description
    @@ -3225,7 +3397,7 @@ DVX_ABI_STAMP(mylib);

    Raw Scancodes

    KEY_SCAN_* are the raw BIOS scancodes (without KEY_EXT_FLAG): KEY_SCAN_ESC, KEY_SCAN_TAB, KEY_SCAN_SPACE, KEY_SCAN_F1 .. KEY_SCAN_F12, KEY_SCAN_HOME, KEY_SCAN_END, KEY_SCAN_PGUP, KEY_SCAN_PGDN, KEY_SCAN_INSERT, KEY_SCAN_DELETE, KEY_SCAN_UP, KEY_SCAN_DOWN, KEY_SCAN_LEFT, KEY_SCAN_RIGHT, KEY_SCAN_CTRL_LEFT, KEY_SCAN_CTRL_RIGHT, KEY_SCAN_CTRL_END, KEY_SCAN_CTRL_HOME, KEY_SCAN_ALT_TAB.

    Raw Key Codes

    -

    A raw key code names one physical key: its set-1 make code (1-127), plus KEY_RAW_EXT (0x80) for keys sent behind an 0xE0 prefix (grey arrows and navigation keys, right Ctrl and Alt, keypad Enter). KEY_RAW_CODE(scancode, extended) builds one. The code is the same whatever modifier is held; letters, digits and punctuation use their KEY_SCAN_* / BIOS scancode. Used by the held-key table (platformKeyIsDown, gameKeyIsDown) and RawKeyEventT.

    +

    A raw key code names one physical key: its set-1 make code (1-127), plus KEY_RAW_EXT (0x80) for keys sent behind an 0xE0 prefix (grey arrows and navigation keys, right Ctrl and Alt, keypad Enter). KEY_RAW_CODE(scancode, extended) builds one. The code is the same whatever modifier is held; letters, digits, and punctuation use their KEY_SCAN_* / BIOS scancode. Used by the held-key table (platformKeyIsDown, gameKeyIsDown) and RawKeyEventT.

      Define                          Value   Key
       ------                          -----   ---
       KEY_RAW_LSHIFT, KEY_RAW_RSHIFT  0x2A, 0x36   Left / right Shift
    @@ -3313,7 +3485,123 @@ DVX_ABI_STAMP(mylib);
    ------ ----- ----------- FONT_NUM_CHARS 256 Glyph count (full 8-bit character range) FONT_CHAR_WIDTH 8 Fixed glyph width in pixels (defined in dvxTypes.h) - FONT_CHAR_HEIGHT 16 Glyph height in pixels (defined in dvxTypes.h) + FONT_CHAR_HEIGHT 16 Glyph height in pixels (defined in dvxTypes.h) + FONT_SYS_ASCENT 12 Rows above the baseline (letters sit on row 11) + FONT_SYS_UNDERLINE_Y 15 Underline and accelerator mark row (the cell's last) + FONT_SYS_STRIKE_Y 8 Strike-through row (the middle of the lower-case letters) + FONT_SYS_LINE_THICK 1 Rows in those lines +
    +
    +

    dvxText.h -- Fonts

    +

    The font families DVX draws with: "System", the built-in 8x16 font, and the families in the FONTS folder. Which families that folder holds depends on the install -- Setup puts in none, the base set (DejaVu Sans, Sans Mono, and Serif), or all of them -- and dvxFontOpen of a family that is not there gives the system font. A family has up to four faces (regular, bold, italic, bold italic), each a .DFN file holding the face at the pixel sizes it was made at; the files are made on the build host from TrueType, Type 1, and bitmap fonts (src/tools/mkfont.c), so DVX only copies bitmaps.

    +

    dvxFontOpen gives a BitmapFontT every text call draws and measures with. It never scales: it picks the face's size nearest the one asked for. A style the family has no face for is made from the nearest face -- bold by thickening each stroke a column (two from 24 pixels), italic by slanting a column per four rows -- and the font's flags say so. Every character of code page 437 draws: what a face lacks is filled in when the font loads. Box drawing, block, and shade characters are drawn to fill the character cell (the pitch, or the digit 0's width) so lines join, also in place of a face's own when its file asks; other characters come from FONT_FALLBACK_FAMILY (FONT_FALLBACK_FIXED for a fixed-pitch face, centered in its pitch) at the nearest size and the same style, or from the system font when that family is not installed.

    +

    A face can also hold a size smoothed (anti-aliased): each pixel one of four levels, from none to full, drawn as that blend of the text color and what is under it. When smoothing is on (dvxFontSetSmooth) such a size opens smoothed, with FONT_FLAG_GRAY set and FONT_GRAY_BPP bits per pixel; other sizes and faces stay 1-bit. At 8 bits per pixel there are no colors to blend: the pixels of FONT_GRAY_HALF and more are drawn, the rest not.

    +

    Fonts are shared and counted: the same family, size, and style opened again is the same font, and dvxFontClose gives one up. A few fonts no one holds stay loaded so reopening them is quick. A font's memory belongs to DVX, not to the program that opened it. The family list is read from DVX_FONTS_DIR the first time it is needed; a file that cannot be read is logged and skipped.

    +

    Fonts a Program Carries

    +

    A program can carry fonts of its own: font resources in its file (DVX_RES_FONT, dvxRes.h; a .res line "name font FILE.DFN", or a BASIC project's Fonts), .DFN files it adds with dvxFontAddFile, or fonts in memory (dvxFontAddMemory). The shell adds a program's font resources when it loads the program and forgets them when the program ends; whatever a program adds itself goes then too. Its fonts are its own: every call here works for the running program (the app id the shell sets while a program's code runs, window callbacks included), and other programs never see them.

    +

    The program sees the installed families and its own. Where it has a family that is installed too, its faces and sizes come first, and the installed family fills in the styles and sizes it lacks: a program that carries DejaVu Sans Bold at 36 pixels gets that size from its own file and every other size and style of DejaVu Sans from the FONTS folder. The nearest size is chosen from both together. Fonts built for a program that carries fonts are cached for it alone; programs that carry none share every font.

    +

    A .DFN carries its notice (where the font came from and its copyright notices) and its licence, so they go wherever the font goes; dvxFontLicence reads them, and the Font dialog shows them (its Licence button).

    +

    Constants

    +
      Define                  Value                 Description
    +  ------                  -----                 -----------
    +  DVX_FONTS_DIR           "FONTS"               The folder of .DFN files (beside CONFIG)
    +  FONT_SYSTEM_FAMILY      "System"              The built-in 8x16 font's family name
    +  FONT_FALLBACK_FAMILY    "DejaVu Sans"         Fills what a proportional face lacks
    +  FONT_FALLBACK_FIXED     "DejaVu Sans Mono"    Fills what a fixed-pitch face lacks
    +

    dvxFontOpen

    +
    BitmapFontT *dvxFontOpen(const char *family, int32_t pixelSize, uint32_t style);
    +

    The font of family at the size nearest pixelSize in style. NULL, "", and a family there is none of give the system font (16 pixels; plain, it is dvxFont8x16 itself, drawn by the fast fixed paths).

    +
      Parameter   Description
    +  ---------   -----------
    +  family      Family name (any case)
    +  pixelSize   Size wanted, in pixels
    +  style       FONT_STYLE_BOLD and / or FONT_STYLE_ITALIC (the line styles are ignored)
    +

    Returns: The font, or NULL when out of memory. Close it with dvxFontClose.

    +

    dvxFontClose

    +
    void dvxFontClose(BitmapFontT *font);
    +

    Give up a font dvxFontOpen gave. NULL and fonts not from dvxFontOpen (dvxFont8x16, the application context's font) are ignored.

    +

    dvxFontFamilyCount / dvxFontFamilyName

    +
    int32_t     dvxFontFamilyCount(void);
    +const char *dvxFontFamilyName(int32_t index);
    +

    The number of families (the system font's included) and family index: "System" first, then the others in alphabetical order. dvxFontFamilyName returns NULL for an index out of range.

    +

    dvxFontFamilyOf

    +
    const char *dvxFontFamilyOf(const BitmapFontT *font);
    +

    The family a font was opened from; "System" for the system font and for fonts not from dvxFontOpen.

    +

    dvxFontFamilyFixed

    +
    bool dvxFontFamilyFixed(const char *family);
    +

    True when every face of family is fixed-pitch (the system font is; a family there is none of is not): the families an editor or terminal can use.

    +

    dvxFontSizes

    +
    int32_t dvxFontSizes(const char *family, uint32_t style, int32_t *sizes, int32_t max);
    +

    The pixel sizes family has in style -- those of the face the style is drawn from -- smallest first, into sizes (at most max).

    +

    Returns: How many sizes there are (which may be more than max); 0 for a family there is none of.

    +

    dvxFontRetain

    +
    BitmapFontT *dvxFontRetain(BitmapFontT *font);
    +

    Hold font once more, for keeping a font someone else opened (each hold is given up with dvxFontClose). Returns font; fonts not from dvxFontOpen are returned as they are.

    +

    dvxFontRescan

    +
    void dvxFontRescan(void);
    +

    Read the FONTS folder again on the next call, after fonts were added or removed. The fonts no one holds are unloaded; open fonts stay valid.

    +

    dvxFontSetSmooth / dvxFontSmooth

    +
    void dvxFontSetSmooth(bool smooth);
    +bool dvxFontSmooth(void);
    +

    Smooth fonts where their face has the size smoothed, or stop; dvxFontSmooth says which it is (off at first). The fonts open are built again in place: the same BitmapFontT pointers with new glyphs and a new serial, so code that keeps measurements of a font compares its serial too, and text is laid out again. The fonts no one holds are unloaded. Programs call dvxSetFontSmoothing (dvxApp.h) instead, which follows the user's choice and the screen's depth.

    +

    dvxFontAddFile / dvxFontAddMemory

    +
    bool dvxFontAddFile(int32_t owner, const char *path);
    +bool dvxFontAddMemory(int32_t owner, const void *data, uint32_t length, const char *name);
    +

    Add the .DFN at path, or held in data (length bytes, copied at once; name names it in the log), as a face of its family for program owner (an app id; 0 for every program, as SETUP.EXE adds the fonts it carries). Families added for owner 0 are seen by every program and kept by dvxFontRescan.

    +

    Returns: True when added; false, logged, when the font cannot be read, calls its family "System", or owner has a face of that family and style already.

    +

    dvxFontAddResources

    +
    int32_t dvxFontAddResources(int32_t owner, const char *path);
    +

    Add every font resource (DVX_RES_FONT) of the file at path -- a program, a library, or a resource file -- for owner. The shell calls it for each program it loads.

    +

    Returns: How many fonts were added.

    +

    dvxFontRemoveOwner

    +
    void dvxFontRemoveOwner(int32_t owner);
    +

    Forget every face added for owner (the shell does when a program ends; for 0, the faces added for every program, the installed ones staying). Its fonts no one holds are unloaded; a held one stays as it is until it is closed.

    +

    dvxFontLicence

    +
    char *dvxFontLicence(const BitmapFontT *font);
    +

    The texts that go with the face font was built from: its notice -- the family and style, the fonts it was converted from, and their copyright notices -- then a blank line and its licence. ASCII with CR LF line ends; malloc'd (the caller frees it).

    +

    Returns: The texts; NULL for the system font, and when the font's file cannot be read.

    +
    +
    +

    dvxFlow.h -- Flowing Text

    +

    A flow is a document built in order: runs of text, each in a font and a color, line breaks, and paragraph starts, each with an alignment. dvxFlowLayout wraps it to a width into lines; dvxFlowPaint draws them. The RichLabel widget is a flow with calls around it; the RichEdit widget lays out each of its paragraphs as a flow and edits through the offset calls below.

    +

    Layout is greedy: a word and the spaces after it go on the line when the word fits, else the line ends and the word starts the next one, without its leading spaces. A word wider than a whole line is broken where the line is full. A line is as tall as the tallest font on it -- the largest ascent above one baseline and the deepest descent below it -- and its trailing spaces neither count toward its width (alignment) nor draw (an underline would show them). A word may go on across runs (a style changed inside it, or punctuation in a run of its own); it wraps as one word, and only a word wider than a whole line breaks, where the line is full. Underline and strike-through come with a run's font (dvxText.h).

    +

    Offsets number the text added from 0, each run's characters after the one before (breaks and paragraphs take none); an offset is the caret before that character. Each line owns the offsets from its start to the next line's start -- the spaces it wrapped at included -- and the last line those to the text's end. Text that is all empty lays out as one empty line in its first run's font, so an empty paragraph keeps a height and a caret place.

    +

    Constants

    +
      Define               Value          Description
    +  ------               -----          -----------
    +  FLOW_ALIGN_LEFT      0              Paragraph alignment
    +  FLOW_ALIGN_CENTER    1
    +  FLOW_ALIGN_RIGHT     2
    +  FLOW_COLOR_DEFAULT   0xFFFFFFFF     A run in the color dvxFlowPaint is given
    +  FLOW_WIDTH_NATURAL   INT32_MAX      dvxFlowLayout's width for the natural size: lines break only at breaks and paragraphs
    +

    Functions

    +
      Function                                                     Description
    +  --------                                                     -----------
    +  TextFlowT *dvxFlowCreate(void)                               An empty flow (one left-aligned paragraph); NULL out of memory.
    +  void dvxFlowDestroy(flow)                                    Free it and give up the fonts it holds. NULL is ignored.
    +  void dvxFlowClear(flow)                                      Empty it again.
    +  bool dvxFlowAddText(flow, text, len, font, color)            Add len bytes of text (len below 0: up to the NUL) in font and color (0x00RRGGBB or FLOW_COLOR_DEFAULT). The flow holds the font (dvxFontRetain) and copies the text.
    +  bool dvxFlowAddBreak(flow)                                   Start a new line in the same paragraph (two in a row make an empty line, as tall as the font before it).
    +  bool dvxFlowAddParagraph(flow, align)                        Start a new paragraph aligned FLOW_ALIGN_*; it ends the line before it when that has text, and on an empty flow it sets the first paragraph's alignment.
    +  int32_t dvxFlowLayout(flow, width)                           Lay it out width pixels wide; returns its height. The same width again costs nothing.
    +  int32_t dvxFlowHeight(flow) / dvxFlowWidth(flow)             The last layout's height and widest line.
    +  void dvxFlowPaint(flow, d, ops, x, y, defaultFg)             Draw the lines that meet the clip rectangle, top-left at x, y, aligned within the width laid out at (the widest line's for FLOW_WIDTH_NATURAL); FLOW_COLOR_DEFAULT runs in defaultFg (a packed pixel). Transparent.
    +  void dvxFlowPaintSel(flow, d, ops, x, y, defaultFg, selStart, selEnd, selFg, selBg)   dvxFlowPaint with offsets selStart to selEnd selected: their text in selFg on selBg (packed pixels), a line's undrawn trailing spaces too.
    +  int32_t dvxFlowTextLength(flow)                              The characters added (the last offset).
    +  int32_t dvxFlowLineCount(flow)                               The last layout's lines.
    +  int32_t dvxFlowLineOf(flow, offset)                          The line the caret before offset is on (a line's end is the next line's start); -1 with no lines.
    +  int32_t dvxFlowLineStart(flow, line)                         The offset a line starts at; past the last line, the text's length.
    +  void dvxFlowLineBox(flow, line, &y, &h)                      A line's top and height.
    +  void dvxFlowCaret(flow, offset, &x, &y, &h)                  The caret before offset: its x, and its line's top and height (alignment included).
    +  int32_t dvxFlowHitTest(flow, x, y)                           The offset nearest a point: on the line at y (the first or last beyond them), the character boundary nearest x. A line that wrapped at a space ends before it.
    +
    TextFlowT   *flow = dvxFlowCreate();
    +BitmapFontT *bold = dvxFontOpen("DejaVu Sans", 14, FONT_STYLE_BOLD);
    +
    +dvxFlowAddText(flow, "Warning: ", -1, bold, DVX_RGB(160, 0, 0));
    +dvxFlowAddText(flow, "the disk is almost full.", -1, &ctx->font, FLOW_COLOR_DEFAULT);
    +dvxFontClose(bold);
    +dvxFlowLayout(flow, 200);
    +dvxFlowPaint(flow, d, ops, x, y, fg);

    dvxPal.h -- 256-Color Palette for 8-bit Mode

    @@ -3422,7 +3710,7 @@ DVX_ABI_STAMP(mylib);

    All 2D drawing operations: rectangle fills, bitmap blits, text rendering, bevels, lines, and cursor rendering. Every function draws into the display's backbuffer and clips to the current clip rectangle. This layer is stateless beyond the clip rect on DisplayT.

    drawInit

    void drawInit(BlitOpsT *ops, const DisplayT *d);
    -

    Populate a BlitOpsT with the correct span functions for the display's pixel depth (1, 2 or 4 bytes per pixel). Must be called once after videoInit(). For any other pixel size the span functions are left NULL so misuse fails loudly instead of drawing with the wrong depth.

    +

    Populate a BlitOpsT with the correct span functions for the display's pixel depth (1, 2, or 4 bytes per pixel). Must be called once after videoInit(). For any other pixel size the span functions are left NULL so misuse fails loudly instead of drawing with the wrong depth.

      Parameter   Description
       ---------   -----------
       ops         BlitOpsT to populate
    @@ -3492,7 +3780,7 @@ DVX_ABI_STAMP(mylib);
    ch Character to draw fg, bg Foreground and background packed colors opaque true = fill background, false = transparent -

    Returns: Advance width (always charWidth).

    +

    Returns: Advance width (charWidth for the system font, the glyph's advance for a loaded font).

    drawText

    void drawText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque);

    Draw a null-terminated string. Equivalent to drawTextN over the whole string (one background span per scanline, character-range clipping).

    @@ -3532,12 +3820,35 @@ DVX_ABI_STAMP(mylib); fg Packed text color

    textWidth

    int32_t textWidth(const BitmapFontT *font, const char *text);
    -

    Return the pixel width of a null-terminated string (strlen(text) * charWidth).

    +

    Return the pixel width of a null-terminated string: the sum of its characters' advances (for the system font strlen(text) * charWidth).

      Parameter   Description
       ---------   -----------
       font        Bitmap font
       text        Null-terminated string

    Returns: Width in pixels.

    +

    textWidthN

    +
    int32_t textWidthN(const BitmapFontT *font, const char *text, int32_t count);
    +

    The pixel width of the first count characters of text (0 when count is 0 or less): where a caret after them goes, how much of a line fits.

    +
      Parameter   Description
    +  ---------   -----------
    +  font        Bitmap font
    +  text        Characters (not required to be null-terminated)
    +  count       How many to measure
    +

    Returns: Width in pixels.

    +

    drawTextStyled

    +
    void drawTextStyled(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t style, uint32_t fg, uint32_t bg, bool opaque);
    +void drawTextStyledN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t count, uint32_t style, uint32_t fg, uint32_t bg, bool opaque);
    +

    Draw text (drawText, drawTextN) with the lines style asks for: FONT_STYLE_UNDERLINE at the font's underlineY and FONT_STYLE_STRIKE at its strikeY, lineThick rows, as wide as the text, in fg. Bold and italic are the font's own (dvxFontOpen); those bits of style are ignored here.

    +
      Parameter   Description
    +  ---------   -----------
    +  d           Display context
    +  ops         Blit operations vtable
    +  font        Bitmap font
    +  x, y        Top-left of the line
    +  text        Text (drawTextStyledN: count characters of it)
    +  style       FONT_STYLE_UNDERLINE and / or FONT_STYLE_STRIKE
    +  fg, bg      Foreground and background packed colors
    +  opaque      true = fill background, false = transparent

    accelParse

    char accelParse(const char *text);

    Scan text for an & prefix and return the following character as a lowercase accelerator key. "&File" returns 'f', "E&xit" returns 'x'.

    @@ -3547,7 +3858,7 @@ DVX_ABI_STAMP(mylib);

    Returns: Lowercase accelerator character, or 0 if none.

    drawTextAccel

    void drawTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque);
    -

    Draw text with & accelerator markers. The character after & is drawn underlined to indicate the keyboard shortcut. && produces a literal &. Used for menu items and button labels.

    +

    Draw text with & accelerator markers. The character after & is marked with the font's underline (underlineY; the system font's last row) across its advance to indicate the keyboard shortcut. && produces a literal &. Used for menu items and button labels.

      Parameter   Description
       ---------   -----------
       d           Display context
    @@ -3559,7 +3870,7 @@ DVX_ABI_STAMP(mylib);
    opaque true = fill background, false = transparent

    textWidthAccel

    int32_t textWidthAccel(const BitmapFontT *font, const char *text);
    -

    Measure text width excluding & markers (so "&File" measures as 4 chars).

    +

    Measure text width excluding & markers (so "&File" measures as the width of "File").

      Parameter   Description
       ---------   -----------
       font        Bitmap font
    @@ -3599,7 +3910,7 @@ DVX_ABI_STAMP(mylib);
    cursorCol Column for inverted text cursor (-1 = none)

    drawArrowGlyph

    void drawArrowGlyph(DisplayT *d, const BlitOpsT *ops, int32_t cx, int32_t cy, int32_t rows, ScrollArrowDirE dir, uint32_t color);
    -

    Draw a solid triangle of rows rows (base 2 * rows - 1 pixels) pointing in direction dir (ScrollArrowUpE/DownE/LeftE/RightE), centered on (cx, cy). The shared direction glyph for scrollbar arrows, spinners, tab scroll buttons, navigation bars and sort indicators.

    +

    Draw a solid triangle of rows rows (base 2 * rows - 1 pixels) pointing in direction dir (ScrollArrowUpE/DownE/LeftE/RightE), centered on (cx, cy). The shared direction glyph for scrollbar arrows, spinners, tab scroll buttons, navigation bars, and sort indicators.

      Parameter   Description
       ---------   -----------
       d           Display context
    @@ -3610,7 +3921,7 @@ DVX_ABI_STAMP(mylib);
    color Fill color

    drawScrollbar

    void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, ScrollbarOrientE orient, int32_t x, int32_t y, int32_t length, int32_t barW, int32_t thumbPos, int32_t thumbSize);
    -

    Paint a complete scrollbar from a ready-made thumb geometry: the sunken trough, an arrow button (drawScrollbarArrowButton) at each end, and a raised thumb thumbPos pixels past the first arrow button. The window scrollbars, the widget scrollbars and wmDrawScrollbarAt (which computes the thumb from a scroll state) all paint through it.

    +

    Paint a complete scrollbar from a ready-made thumb geometry: the sunken trough, an arrow button (drawScrollbarArrowButton) at each end, and a raised thumb thumbPos pixels past the first arrow button. The window scrollbars, the widget scrollbars, and wmDrawScrollbarAt (which computes the thumb from a scroll state) all paint through it.

      Parameter   Description
       ---------   -----------
       d           Display context
    @@ -3895,7 +4206,7 @@ void drawFocusRectDots(int32_t x, int32_t y, int32_t w, int32_t h, DrawDotFnT fn
       enabled     true = enabled, false = grayed out

    wmMenuItemSetLabel

    void wmMenuItemSetLabel(MenuItemT *item, const char *label);
    -

    Give an item a new label, written as wmAddMenuItem takes it: & before the accelerator letter, and the key text after a tab ("&Open\tCtrl+O"). The accelerator follows the new label; the item's ID, type and state stay. An open popup shows the new label the next time it is drawn. A NULL item or label does nothing.

    +

    Give an item a new label, written as wmAddMenuItem takes it: & before the accelerator letter, and the key text after a tab ("&Open\tCtrl+O"). The accelerator follows the new label; the item's ID, type, and state stay. An open popup shows the new label the next time it is drawn. A NULL item or label does nothing.

    wmAddSubMenu

    MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label);

    Create a cascading submenu attached to a parent menu. The child MenuT is heap-allocated and freed when the parent window is destroyed.

    @@ -4201,11 +4512,12 @@ void drawFocusRectDots(int32_t x, int32_t y, int32_t w, int32_t h, DrawDotFnT fn uint32_t windowsCreated Windows created by dvxCreateWindow so far (the shell compares it before and after a close request to see whether the app showed a prompt) int32_t currentAppId App charged for allocations and callbacks (0 = shell) bool busy Busy cursor shown and input blocked + bool fontSmoothing Text smoothing wanted (dvxSetFontSmoothing; on at start) idleCallback / idleCtx Called instead of yielding when a frame had no events onCtrlEsc, onF1, onTitleChange Shell hooks (with their ctx pointers) VideoModeInfoT *videoModes stb_ds array of settable modes char wallpaperPath[] Current wallpaper source path (empty = none) - WallpaperModeE wallpaperMode Stretch, tile or center + WallpaperModeE wallpaperMode Stretch, tile, or center

    dvxSetMouseConfig programs the mouse speed and acceleration into the platform layer, which re-applies them after every mouse driver reset; mouseAccel and mouseMickeys only record the last values applied (for dvxMouseToPrefs and settings pages).

    Initialization and Shutdown

    dvxInit

    @@ -4291,7 +4603,7 @@ void drawFocusRectDots(int32_t x, int32_t y, int32_t w, int32_t h, DrawDotFnT fn win Window to raise

    dvxRequestWindowClose

    void dvxRequestWindowClose(AppContextT *ctx, WindowT *win);
    -

    Ask a window to close exactly as its close box, Alt+F4 and the system menu's Close do. The window's onClose runs (under the window's app ID) and decides -- a save prompt may keep the window open; a window without onClose is destroyed. A clean frame is composited first so an open menu is erased before onClose shows a dialog. When no dispatch is in progress the window may already be freed on return, so look it up again by its id rather than using the pointer. The shell's Exit DVX sequence uses this to ask each app to close.

    +

    Ask a window to close exactly as its close box, Alt+F4, and the system menu's Close do. The window's onClose runs (under the window's app ID) and decides -- a save prompt may keep the window open; a window without onClose is destroyed. A clean frame is composited first so an open menu is erased before onClose shows a dialog. When no dispatch is in progress the window may already be freed on return, so look it up again by its id rather than using the pointer. The shell's Exit DVX sequence uses this to ask each app to close.

      Parameter   Description
       ---------   -----------
       ctx         Application context
    @@ -4483,7 +4795,7 @@ void drawFocusRectDots(int32_t x, int32_t y, int32_t w, int32_t h, DrawDotFnT fn
       ctx         Application context

    dvxLoadTheme

    bool dvxLoadTheme(AppContextT *ctx, const char *filename);
    -

    Load a theme file (INI format with [colors] section) and apply it through dvxColorsFromPrefs. Each color is a "name = r,g,b" key in [colors]; keys outside that section, unknown names and malformed values are ignored, and channels are clamped to 0..255. A missing file returns false.

    +

    Load a theme file (INI format with [colors] section) and apply it through dvxColorsFromPrefs. Each color is a "name = r,g,b" key in [colors]; keys outside that section, unknown names, and malformed values are ignored, and channels are clamped to 0..255. A missing file returns false.

      Parameter   Description
       ---------   -----------
       ctx         Application context
    @@ -4500,7 +4812,7 @@ void drawFocusRectDots(int32_t x, int32_t y, int32_t w, int32_t h, DrawDotFnT fn
     

    dvxColorsFromPrefs / dvxColorsToPrefs

    bool dvxColorsFromPrefs(AppContextT *ctx, PrefsHandleT *h);
     void dvxColorsToPrefs(const AppContextT *ctx, PrefsHandleT *h);
    -

    The [colors] section of an INI handle, the one place its keys are read and written: dvxLoadTheme / dvxSaveTheme, the shell at start (DVX.INI) and Control Panel (OK, and its Cancel snapshot in a prefsCreate handle) all use these. FromPrefs reads every "name = r,g,b" key (malformed ones ignored, channels clamped to 0..255), repaints only when a color changed and returns true when any color key was read. ToPrefs writes every color.

    +

    The [colors] section of an INI handle, the one place its keys are read and written: dvxLoadTheme / dvxSaveTheme, the shell at start (DVX.INI), and Control Panel (OK, and its Cancel snapshot in a prefsCreate handle) all use these. FromPrefs reads every "name = r,g,b" key (malformed ones ignored, channels clamped to 0..255), repaints only when a color changed and returns true when any color key was read. ToPrefs writes every color.

    dvxColorName

    const char *dvxColorName(ColorIdE id);

    Return the INI key name for a color ID (e.g. "desktop", "windowFace").

    @@ -4535,6 +4847,14 @@ void dvxColorsToPrefs(const AppContextT *ctx, PrefsHandleT *h);
    bool dvxWallpaperFromPrefs(AppContextT *ctx, PrefsHandleT *h);
     void dvxWallpaperToPrefs(const AppContextT *ctx, PrefsHandleT *h);

    The [desktop] wallpaper and mode keys of an INI handle (mode through prefsWallpaperChoices). FromPrefs applies them: a missing mode is WALLPAPER_MODE_DEFAULT and a missing wallpaper means none; the image is built again only when the path or mode differs from the current one. It returns false when the named image could not be loaded. ToPrefs writes the path (removes the key when there is none) and the mode.

    +

    Text Smoothing

    +

    dvxSetFontSmoothing

    +
    void dvxSetFontSmoothing(AppContextT *ctx, bool on);
    +

    Smooth text, or stop (ctx->fontSmoothing; on after dvxInit). Text is smoothed when on and the screen has more than 8 bits per pixel (dvxFontSetSmooth, dvxText.h), in the faces and sizes that have a smoothed strike; dvxChangeVideoMode applies it again at the new depth. When what is drawn changes, every window is laid out and painted again.

    +

    dvxFontsFromPrefs / dvxFontsToPrefs

    +
    void dvxFontsFromPrefs(AppContextT *ctx, PrefsHandleT *h);
    +void dvxFontsToPrefs(const AppContextT *ctx, PrefsHandleT *h);
    +

    The [fonts] keys of an INI handle: smoothing (true or false; true when missing). FromPrefs applies it with dvxSetFontSmoothing; ToPrefs writes the setting. The shell uses FromPrefs at start and Control Panel both for OK and for its Cancel snapshot.

    Mouse Configuration

    dvxSetMouseConfig

    void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio, int32_t wheelStep);
    @@ -4547,17 +4867,17 @@ void dvxWallpaperToPrefs(const AppContextT *ctx, PrefsHandleT *h); accelThreshold Double-speed threshold in mickeys/sec (0 = don't change) mickeyRatio Mickeys per 8 pixels (0 = don't change, 8 = default speed) wheelStep Lines to scroll per wheel notch (1-10, default 3) -

    The speed, acceleration and wheel settings are re-applied by the platform layer after every mouse driver reset, so they survive a live video-mode change. The values are kept in AppContextT (wheelDirection, dblClickMs, mouseAccel, mouseMickeys, wheelStep).

    +

    The speed, acceleration, and wheel settings are re-applied by the platform layer after every mouse driver reset, so they survive a live video-mode change. The values are kept in AppContextT (wheelDirection, dblClickMs, mouseAccel, mouseMickeys, wheelStep).

    dvxMouseFromPrefs / dvxMouseToPrefs

    void dvxMouseFromPrefs(AppContextT *ctx, PrefsHandleT *h);
     void dvxMouseToPrefs(const AppContextT *ctx, PrefsHandleT *h);
    -

    The [mouse] keys of an INI handle: wheel and acceleration (by name, through prefsWheelDirChoices and prefsAccelChoices), doubleclick (milliseconds), speed (mickeys per 8 pixels) and wheelspeed. FromPrefs applies them with dvxSetMouseConfig, defaults for missing keys (MOUSE_*_DEFAULT); ToPrefs writes the configuration in effect (an acceleration no choice has leaves that key alone). The shell uses FromPrefs at start and Control Panel both for OK and for its Cancel snapshot.

    +

    The [mouse] keys of an INI handle: wheel and acceleration (by name, through prefsWheelDirChoices and prefsAccelChoices), doubleclick (milliseconds), speed (mickeys per 8 pixels), and wheelspeed. FromPrefs applies them with dvxSetMouseConfig, defaults for missing keys (MOUSE_*_DEFAULT); ToPrefs writes the configuration in effect (an acceleration no choice has leaves that key alone). The shell uses FromPrefs at start and Control Panel both for OK and for its Cancel snapshot.

    Input Entropy

    dvxSetInputEntropyHook

    typedef void (*DvxEntropyHookT)(const uint8_t *data, int32_t len);
     
     void dvxSetInputEntropyHook(DvxEntropyHookT fn);
    -

    Install a sink for input-timing entropy (for example a random-number generator's add-entropy routine). While a hook is set, every keyboard event and every mouse change (movement, button, wheel) is recorded as a 32-bit platformEntropyStamp value followed by the event bytes (scancode, ASCII and shift state; or position, buttons and wheel delta). Records are batched and handed to fn once 64 bytes have accumulated, so the cost per event stays negligible. An idle mouse is not recorded. Changing or clearing the hook discards any pending partial batch. The core library has no link-time dependency on any consumer; the shell resolves one at run time.

    +

    Install a sink for input-timing entropy (for example a random-number generator's add-entropy routine). While a hook is set, every keyboard event and every mouse change (movement, button, wheel) is recorded as a 32-bit platformEntropyStamp value followed by the event bytes (scancode, ASCII, and shift state; or position, buttons, and wheel delta). Records are batched and handed to fn once 64 bytes have accumulated, so the cost per event stays negligible. An idle mouse is not recorded. Changing or clearing the hook discards any pending partial batch. The core library has no link-time dependency on any consumer; the shell resolves one at run time.

      Parameter   Description
       ---------   -----------
       fn          Entropy sink, or NULL to disable
    @@ -4591,7 +4911,7 @@ void dvxBeep(void); void dvxSetSoundHook(DvxSoundHookT fn);

    The function that plays the sounds, set by the sound library when it loads (NULL: silent). Programs do not call it.

    Full-Screen Exclusive Mode

    -

    A window's program takes the whole screen in a video mode of its own (a game at 320x200, say) while every other program keeps running: windows keep painting into their own buffers, and the desktop is drawn again when the program lets go. The owner's window must have the focus: when it loses it (Alt+Tab, Ctrl+Esc, a dialog, another window coming to the front) the desktop comes back and onChange hears ExclusiveSuspendedE; when the window has the focus again the mode is set again and onChange hears ExclusiveActiveE (draw the whole frame: the screen and the backbuffer start black). While active every key but Alt+Tab, Ctrl+Esc and Alt+F4 goes to the window as in game input mode, the mouse cursor is not drawn, onMouse gets full-screen coordinates, dvxScreenshot saves the full screen, and dvxChangeVideoMode fails. Closing the window, the program ending or being ended, and dvxExclusiveEnd give the screen back. See the Writing Games guide.

    +

    A window's program takes the whole screen in a video mode of its own (a game at 320x200, say) while every other program keeps running: windows keep painting into their own buffers, and the desktop is drawn again when the program lets go. The owner's window must have the focus: when it loses it (Alt+Tab, Ctrl+Esc, a dialog, another window coming to the front) the desktop comes back and onChange hears ExclusiveSuspendedE; when the window has the focus again the mode is set again and onChange hears ExclusiveActiveE (draw the whole frame: the screen and the backbuffer start black). While active every key but Alt+Tab, Ctrl+Esc, and Alt+F4 goes to the window as in game input mode, the mouse cursor is not drawn, onMouse gets full-screen coordinates, dvxScreenshot saves the full screen, and dvxChangeVideoMode fails. Closing the window, the program ending or being ended, and dvxExclusiveEnd give the screen back. See the Writing Games guide.

    typedef enum {
         ExclusiveActiveE,       // the screen is the program's: draw the whole frame
         ExclusiveSuspendedE,    // its window lost the focus: the desktop is back until it has the focus again
    @@ -4605,7 +4925,7 @@ typedef void (*ExclusiveFnT)(WindowT *win, ExclusiveStateE state, void *userData
     

    Returns: DVX_EXCL_OK; DVX_EXCL_ERR_ARGS (no such window, or a bad size); DVX_EXCL_ERR_BUSY (another window has full screen); DVX_EXCL_ERR_MODE (no mode could be set; the desktop is back).

    dvxExclusiveEnd

    void dvxExclusiveEnd(AppContextT *ctx, WindowT *win);
    -

    Give the screen back: the desktop's mode, palette and mouse, and a full repaint. onChange is not called. Nothing when win does not have full screen.

    +

    Give the screen back: the desktop's mode, palette, and mouse, and a full repaint. onChange is not called. Nothing when win does not have full screen.

    dvxExclusiveDisplay / dvxExclusiveBlitOps

    DisplayT       *dvxExclusiveDisplay(AppContextT *ctx);
     const BlitOpsT *dvxExclusiveBlitOps(AppContextT *ctx);
    @@ -4659,14 +4979,14 @@ const BlitOpsT *dvxExclusiveBlitOps(AppContextT *ctx);
    cmdId Command whose shortcuts go

    dvxAccelFormat

    void dvxAccelFormat(int32_t key, int32_t modifiers, char *out, int32_t outSize);
    -

    Write a key and its modifiers (given as for dvxAddAccel) as text, the way menus show it after a tab: the modifiers Ctrl, Alt and Shift in that order, each followed by +, then the key -- a letter or digit, F1 to F12, or one of the names Backspace, Del, Down, End, Enter, Esc, Home, Ins, Left, PgDn, PgUp, Right, Space, Tab, Up. Key 0 gives "".

    +

    Write a key and its modifiers (given as for dvxAddAccel) as text, the way menus show it after a tab: the modifiers Ctrl, Alt, and Shift in that order, each followed by +, then the key -- a letter or digit, F1 to F12, or one of the names Backspace, Del, Down, End, Enter, Esc, Home, Ins, Left, PgDn, PgUp, Right, Space, Tab, Up. Key 0 gives "".

    dvxAccelFormat(KEY_ENTER, ACCEL_ALT, text, sizeof(text));   // "Alt+Enter"

    dvxAccelParse

    bool dvxAccelParse(const char *text, int32_t *outKey, int32_t *outMods);
    -

    Read key text as dvxAccelFormat writes it ("F2", "Ctrl+X", "Alt+Enter", "Shift+Del") into a key and ACCEL_* modifiers for dvxAddAccel or dvxRegisterHotkey. Case does not matter, blanks around the parts are ignored, and Delete and Insert are accepted for Del and Ins. Returns false, with *outKey 0, for empty text, an unknown modifier or an unknown key.

    +

    Read key text as dvxAccelFormat writes it ("F2", "Ctrl+X", "Alt+Enter", "Shift+Del") into a key and ACCEL_* modifiers for dvxAddAccel or dvxRegisterHotkey. Case does not matter, blanks around the parts are ignored, and Delete and Insert are accepted for Del and Ins. Returns false, with *outKey 0, for empty text, an unknown modifier, or an unknown key.

    System-Wide Hotkeys

    A hotkey is a keystroke that works whichever window has the focus (or when none has): its callback runs instead of anything the focused window would do with the keys, including its menu keys and accelerators. Program Manager's item shortcut keys are hotkeys. Hotkeys are not checked while a menu or a modal dialog is open, and keys the system handles first (Alt+Tab, Alt+F4, Ctrl+F12, Ctrl+Esc, F1, F10, Alt+Space) never reach them.

    -

    A hotkey belongs to the app that was current when it was registered (ctx->currentAppId, set by the shell whenever app code runs). When an app exits, crashes or is ended from the Task Manager, the shell removes its hotkeys with dvxUnregisterAppHotkeys, so a callback never outlives the code it points into. The callback also runs with currentAppId set to that app.

    +

    A hotkey belongs to the app that was current when it was registered (ctx->currentAppId, set by the shell whenever app code runs). When an app exits, crashes, or is ended from the Task Manager, the shell removes its hotkeys with dvxUnregisterAppHotkeys, so a callback never outlives the code it points into. The callback also runs with currentAppId set to that app.

    dvxRegisterHotkey

    typedef void (*DvxHotkeyFnT)(void *userCtx);
     
    @@ -4723,7 +5043,7 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId);

    Image I/O

    dvxLoadImage

    uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch);
    -

    Load an image file (BMP, PNG, JPEG, GIF) and convert to the display's native pixel format. A file that is no image but a DVX program, widget or library with an icon resource gives that icon (dvxResLoadIconData), so every picture that takes a file name can show a program's icon. Caller must free with dvxFreeImage().

    +

    Load an image file (BMP, PNG, JPEG, GIF) and convert to the display's native pixel format. A file that is no image but a DVX program, widget, or library with an icon resource gives that icon (dvxResLoadIconData), so every picture that takes a file name can show a program's icon. Caller must free with dvxFreeImage().

      Parameter     Description
       ---------     -----------
       ctx           Application context
    @@ -4844,10 +5164,10 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId);

    Returns: Pixel buffer, or NULL if not found.

    dvxResLoadIconData

    void *dvxResLoadIconData(const char *dxePath, uint32_t *outSize);
    -

    The icon of a DVX program, widget or library file as the bytes of its image file: its DVX_RES_NAME_ICON resource ("icon32", a program's icon), else DVX_RES_NAME_WIDGET_ICON ("icon24", a widget's toolbox icon). Decode it with dvxLoadImageFromMemory or dvxLoadImageAlpha. Caller must free() the buffer.

    +

    The icon of a DVX program, widget, or library file as the bytes of its image file: its DVX_RES_NAME_ICON resource ("icon32", a program's icon), else DVX_RES_NAME_WIDGET_ICON ("icon24", a widget's toolbox icon). Decode it with dvxLoadImageFromMemory or dvxLoadImageAlpha. Caller must free() the buffer.

      Parameter     Description
       ---------     -----------
    -  dxePath       Path to the program, widget or library file
    +  dxePath       Path to the program, widget, or library file
       outSize       Output: size of the data in bytes

    Returns: The data, or NULL when the file has neither resource (or is no DVX file).

    dvxResLoadText

    @@ -4880,7 +5200,7 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId);

    dvxDrv.h -- Driver Modules

    -

    One model for the hardware drivers of every device class (sound, joysticks, ...). A driver is a module of its own, a file <name>.drv in its class's folder under bin/libs (built like a library, but the loader does not load it). The library of the device class loads its drivers only when a program first needs that kind of device, probes them, keeps the ones it uses and unloads the rest, so drivers for hardware that is not there, or for a device class nobody uses, never stay in memory. Nothing lists drivers: adding one is dropping its file into the folder.

    +

    One model for the hardware drivers of every device class (sound, joysticks, ...). A driver is a module of its own, a file <name>.drv in its class's folder under bin/libs (built like a library, but the loader does not load it). The library of the device class loads its drivers only when a program first needs that kind of device, probes them, keeps the ones it uses, and unloads the rest, so drivers for hardware that is not there, or for a device class nobody uses, never stay in memory. Nothing lists drivers: adding one is dropping its file into the folder.

    Each driver exports one descriptor under a name its class chooses (audioDriver, joyDriver, ...). The descriptor is the class's own struct and starts with a DvxDriverHeaderT, written with DVX_DRIVER_HDR (the classes wrap it in their own macro, such as AUDIO_DRIVER or JOY_DRIVER).

    Types

      Type               Members
    @@ -4892,7 +5212,7 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId);

    Functions

    dvxDriversLoad

    DvxDriverModT *dvxDriversLoad(const char *dir, const DvxDriverClassT *cls);
    -

    Loads every *.drv in dir (relative to the DVX directory), looks each descriptor up and checks it: the ABI version, the size, a name, and the class's valid function. The valid ones come back as an stb_ds array sorted by priority (highest first, by name on a tie); a module that does not load, has no valid descriptor, or repeats a name a higher priority driver already has is logged to DVX.LOG and unloaded. NULL when there are none.

    +

    Loads every *.drv in dir (relative to the DVX directory), looks each descriptor up, and checks it: the ABI version, the size, a name, and the class's valid function. The valid ones come back as an stb_ds array sorted by priority (highest first, by name on a tie); a module that does not load, has no valid descriptor, or repeats a name a higher priority driver already has is logged to DVX.LOG and unloaded. NULL when there are none.

    dvxDriverKeep

    DvxDriverModT dvxDriverKeep(DvxDriverModT *mods, int32_t index);

    Takes mods[index] out of the list (so dvxDriversFree leaves it loaded) and returns it; unload it with dvxDriverUnload when done.

    @@ -4903,7 +5223,7 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId);
    void dvxDriverUnload(DvxDriverModT *mod);

    Unloads one module (nothing when it is not loaded) and clears its handle and descriptor.

    Writing a Device Class

    -

    A library that has drivers puts them in drivers/<name>/ in its source folder; its Makefile sets OBJDIR and DRVDIR and writes $(foreach d,$(DVX_DRIVERS),$(eval $(call DVX_DRIVER_RULE,$d))) (common.mk), which builds each folder into $(DRVDIR)/<name>.drv. The library defines its descriptor struct (starting with DvxDriverHeaderT), a macro that writes it, and a DvxDriverClassT; it calls dvxDriversLoad when it first needs devices, dvxDriverKeep for the drivers it uses and dvxDriversFree for the rest. Driver code resolves the library's functions when it loads, since the libraries are loaded for everyone to use.

    +

    A library that has drivers puts them in drivers/<name>/ in its source folder; its Makefile sets OBJDIR and DRVDIR and writes $(foreach d,$(DVX_DRIVERS),$(eval $(call DVX_DRIVER_RULE,$d))) (common.mk), which builds each folder into $(DRVDIR)/<name>.drv. The library defines its descriptor struct (starting with DvxDriverHeaderT), a macro that writes it, and a DvxDriverClassT; it calls dvxDriversLoad when it first needs devices, dvxDriverKeep for the drivers it uses, and dvxDriversFree for the rest. Driver code resolves the library's functions when it loads, since the libraries are loaded for everyone to use.

    dvxWgt.h -- Widget System

    @@ -4924,6 +5244,7 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId); WidgetAlignE align Main-axis alignment for children int32_t spacing, padding Tagged sizes for child spacing and padding uint32_t fgColor, bgColor Custom colors (0 = use scheme defaults) + BitmapFontT *font The widget's own font (wgtSetFont); NULL = its container's (wgtFontFor) bool visible, enabled, readOnly State flags bool swallowTab Tab key goes to widget, not focus navigation bool paintDirty Needs repaint (set by wgtInvalidatePaint) @@ -4953,7 +5274,7 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId); onMouseMove(WidgetT *w, int32_t button, int32_t x, int32_t y) Mouse moved onScroll(WidgetT *w, int32_t delta) Mouse wheel onValidate(WidgetT *w) Return false to cancel a write -

    The universal callbacks belong to the widget's host (the application or the form runtime). A widget class sees the same input earlier through its own class methods: WGT_METHOD_ON_MOUSE_EVENT, WGT_METHOD_ON_KEY, WGT_METHOD_ON_KEY_UP, WGT_METHOD_ON_FOCUS, WGT_METHOD_ON_BLUR and WGT_METHOD_ON_WHEEL run before the matching callback (see Method IDs).

    +

    The universal callbacks belong to the widget's host (the application or the form runtime). A widget class sees the same input earlier through its own class methods: WGT_METHOD_ON_MOUSE_EVENT, WGT_METHOD_ON_KEY, WGT_METHOD_ON_KEY_UP, WGT_METHOD_ON_FOCUS, WGT_METHOD_ON_BLUR, and WGT_METHOD_ON_WHEEL run before the matching callback (see Method IDs).

    Size Specification Macros

      Macro            Description
       -----            -----------
    @@ -4977,7 +5298,8 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId);
    WCLASS_PRESS_RELEASE Click = press + release (Button, ImageButton) WCLASS_ACCEL_WHEN_HIDDEN Accelerator matching works even when invisible WCLASS_HAS_TEXT The first field of w->data is the widget's own text (strdup / free) - WCLASS_RAISES_EVENTS The class fires the universal mouse, click, key and scroll callbacks itself (wgtFireEvent); the dispatcher does not call them for its widgets. Its class methods still get every event. A BASIC UserControl has it: its host sees only what the control's code raises. GotFocus / LostFocus still reach onFocus / onBlur. + WCLASS_RAISES_EVENTS The class fires the universal mouse, click, key, and scroll callbacks itself (wgtFireEvent); the dispatcher does not call them for its widgets. Its class methods still get every event. A BASIC UserControl has it: its host sees only what the control's code raises. GotFocus / LostFocus still reach onFocus / onBlur. + WCLASS_FIXED_FONT The class edits text by character columns (TextBox, TextArea, Terminal, ComboBox, Spinner): given a proportional font it draws with the system font instead (wgtFontFor); a fixed-pitch font of any size is used.

    Window Integration

    wgtInitWindow

    WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win);
    @@ -5021,6 +5343,19 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId); --------- ----------- w Widget

    Returns: Text string (empty string if no handler).

    +

    wgtSetFont / wgtFont / wgtFontFor

    +
    bool               wgtSetFont(WidgetT *w, const char *family, int32_t size, uint32_t style);
    +const BitmapFontT *wgtFont(const WidgetT *w);
    +const BitmapFontT *wgtFontFor(const WidgetT *w, const BitmapFontT *inherited);
    +

    wgtSetFont gives w the font of family at the size nearest size in style (dvxFontOpen, dvxText.h: FONT_STYLE_BOLD, ITALIC, UNDERLINE, STRIKE); the widgets inside w draw with it too unless they have their own -- a form's font is its root widget's. A NULL family gives the font up, so w uses its container's again. The window is laid out again. Returns false when out of memory (w keeps the font it had). The font is closed when w is destroyed.

    +

    wgtFontFor is the font w draws with: its own, else the nearest container's up the parent chain, else inherited (the font passed down: the application's, or NULL for a metrics query that needs none). A class with WCLASS_FIXED_FONT given a proportional font gets the system font. The class dispatch helpers (wclsPaint, wclsPaintOverlay, wclsCalcMinSize, wclsLayout, wclsGetLayoutMetrics, wclsGetPopupRect) hand every method this font, so a widget's paint, measure, and layout methods simply use their font parameter. Code that is given no font -- mouse and key handlers, helpers -- calls wgtFont(w), the same with the application's font. Size hints in characters (wgtChars) count the widget's own font.

    +
      Parameter   Description
    +  ---------   -----------
    +  w           Widget
    +  family      Family name (NULL: inherit again)
    +  size        Pixel size wanted
    +  style       FONT_STYLE_* bits
    +  inherited   The font the caller would otherwise use

    wgtSetEnabled

    void wgtSetEnabled(WidgetT *w, bool enabled);

    Enable or disable a widget. Disabled widgets are grayed out and do not receive input; disabling a container also blocks mouse and keyboard input to every widget inside it (see widgetIsEnabledChain). Setting the current value is a no-op.

    @@ -5102,7 +5437,7 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId); void wgtSetPaddingPx(WidgetT *w, int32_t pixels); int32_t wgtGetSpacingPx(const WidgetT *w); void wgtSetSpacingPx(WidgetT *w, int32_t pixels); -

    A container's padding (space inside its edge) and spacing (gap between children) in pixels. The getters return what the layout uses: the container's default while the field is WGT_GAP_DEFAULT (every widget starts so), and 0 for a size given in another unit. The setters clamp to 0..WGT_GAP_PIXELS_MAX (0 is no gap) and re-lay out the window. The BASIC Padding and Spacing properties of VBox, HBox, Frame and ScrollPane use these.

    +

    A container's padding (space inside its edge) and spacing (gap between children) in pixels. The getters return what the layout uses: the container's default while the field is WGT_GAP_DEFAULT (every widget starts so), and 0 for a size given in another unit. The setters clamp to 0..WGT_GAP_PIXELS_MAX (0 is no gap) and re-lay out the window. The BASIC Padding and Spacing properties of VBox, HBox, Frame, and ScrollPane use these.

    wgtLayout

    void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT *font);

    Execute the full two-pass layout algorithm on a tree at an explicit size. Pass 1 (bottom-up): compute minimum sizes; a cap (maxW/maxH) wins over a larger minimum, so a widget whose content asks for more than its cap is given its cap and the rest of the space goes to its siblings. Pass 2 (top-down): allocate space with weighted distribution; a weighted child capped by maxW/maxH stops at its cap and the space it cannot take goes to the other weighted children. Window-managed trees are laid out by wgtInvalidate and the paint path; this is for laying out a tree on demand.

    @@ -5152,7 +5487,7 @@ void wgtSetSpacingPx(WidgetT *w, int32_t pixels);

    Remove the interface registered under name (wgtRegisterIface). A widget class made at run time -- a BASIC program's own UserControl -- leaves the registry this way when its owner is done with it, so the name can be registered again. Nothing happens when no interface has the name.

    wgtUnregisterAppClasses

    void wgtUnregisterAppClasses(int32_t appId);
    -

    Release every class, API (wgtRegisterApi) and interface (wgtRegisterIface) registered while appId was the current app (AppContextT.currentAppId at registration time), since all of them point into the app's module. The shell calls it whenever it unloads an app (normal exit or End Task), once the app's windows and widgets are gone. appId 0 (boot-time widget modules) is ignored.

    +

    Release every class, API (wgtRegisterApi), and interface (wgtRegisterIface) registered while appId was the current app (AppContextT.currentAppId at registration time), since all of them point into the app's module. The shell calls it whenever it unloads an app (normal exit or End Task), once the app's windows and widgets are gone. appId 0 (boot-time widget modules) is ignored.

      Parameter   Description
       ---------   -----------
       appId       App whose classes are released
    @@ -5277,7 +5612,7 @@ static const ButtonApiT sApi = { wclsScrollChildIntoView(parent, child) WGT_METHOD_SCROLL_CHILD_INTO_VIEW Scroll child visible wclsOnBlur(w) WGT_METHOD_ON_BLUR Widget lost keyboard focus wclsOnWheel(w, delta) WGT_METHOD_ON_WHEEL Mouse wheel; true = consumed - wclsOnMouseEvent(w, action, button, x, y, shift) WGT_METHOD_ON_MOUSE_EVENT Every press, release and move (hover too) + wclsOnMouseEvent(w, action, button, x, y, shift) WGT_METHOD_ON_MOUSE_EVENT Every press, release, and move (hover too) wclsOnKeyUp(w, keyCode, shift) WGT_METHOD_ON_KEY_UP Key released on the focused widget wclsOnFocus(w) WGT_METHOD_ON_FOCUS Widget gained keyboard focus

    The central dispatcher calls WGT_METHOD_ON_MOUSE_EVENT for every widget whose class has it, at exactly the points where it calls the universal mouse callbacks and on the same widget (the one under the cursor; inside a non-focusable WCLASS_NO_HIT_RECURSE container, a child with the method gets them itself, and a container with the method keeps them), and always before the callback. action is a WgtMouseActionE: WgtMouseDownE and WgtMouseUpE carry the 1-based button ordinal (1 left, 2 right, 3 middle), WgtMouseMoveE the MOUSE_* mask of the held buttons (0 is a hover move). x and y are content-relative (contentOffX / contentOffY subtracted), shift is the KEY_MOD_* Shift / Ctrl / Alt state. A class that captures the mouse on a press (sets sDragWidget in its WGT_METHOD_ON_MOUSE handler) also gets the moves while the button is held and the left release, wherever the pointer is. WGT_METHOD_ON_KEY_UP runs before onKeyUp with the same arguments, WGT_METHOD_ON_FOCUS before onFocus (the counterpart of ON_BLUR). If a class method destroys widgets, the callback after it is skipped. Classes without these methods see no change.

    @@ -5346,7 +5681,7 @@ static const ButtonApiT sApi = { getFn Getter function pointer (NULL if write-only) setFn Setter function pointer (NULL if read-only) enumNames For WGT_IFACE_ENUM: NULL-terminated array of value names -

    A property is readable when getFn is not NULL and writable when setFn is not NULL -- the one rule for every interface (the form runtime, the designer and the property sheet all apply it). A dynamic interface (WgtIfaceT.dyn) puts WGT_DYN_ACCESS in getFn / setFn for "present"; they are never called. A FLOAT getter returns float and its setter takes float.

    +

    A property is readable when getFn is not NULL and writable when setFn is not NULL -- the one rule for every interface (the form runtime, the designer, and the property sheet all apply it). A dynamic interface (WgtIfaceT.dyn) puts WGT_DYN_ACCESS in getFn / setFn for "present"; they are never called. A FLOAT getter returns float and its setter takes float.

    WgtMethodDescT

    Describes an invokable method on a widget type.

      Field       Description
    @@ -5384,7 +5719,7 @@ static const ButtonApiT sApi = {
       WGT_SIG_STR_STR_BOOL_BOOL  24   bool fn(WidgetT *, const char *, const char *, bool, bool)
       WGT_SIG_RET_INT_INT        25   int32_t fn(const WidgetT *, int32_t)
       WGT_SIG_DYNAMIC            26   bool fn(WidgetT *, const WgtValueT *args, int32_t argc, WgtValueT *ret) (WgtDynMethodFnT): any arguments, as they come
    -

    wgtSigInfo describes each signature (WgtSigInfoT: argc, retType and argTypes[], WGT_SIG_MAX_ARGS = 5; NULL for WGT_SIG_DYNAMIC). wgtIfaceCallMethod is the one place that calls through these signatures.

    +

    wgtSigInfo describes each signature (WgtSigInfoT: argc, retType, and argTypes[], WGT_SIG_MAX_ARGS = 5; NULL for WGT_SIG_DYNAMIC). wgtIfaceCallMethod is the one place that calls through these signatures.

    WgtEventDescT

    Describes an event beyond the standard common set (Click, DblClick, Change, GotFocus, LostFocus, the key and mouse events ...). The widget fires it with wgtFireEvent.

      Field       Description
    @@ -5428,7 +5763,7 @@ static const ButtonApiT sApi = {
             const char *s;   // STRING
         };
     } WgtValueT;
    -

    One property value, method argument or result, or event argument. WGT_IFACE_NONE (5) is "no value" (a void method's result). A STRING value never owns its text: an argument's text belongs to the caller for the duration of the call; a returned one (a property read, a method result) belongs to the widget and stays valid until the next call on that widget or until the widget changes or is destroyed -- copy it to keep it. wgtValueInt, wgtValueBool, wgtValueFloat, wgtValueDouble and wgtValueStr (NULL becomes "") build values. FLOAT is BASIC's Single, DOUBLE its Double: a DOUBLE property or argument keeps every digit (a value through FLOAT is rounded to a float by a static widget's getter and setter, and the form runtime shows it as a Single); as text a DOUBLE has 15 significant digits.

    +

    One property value, method argument or result, or event argument. WGT_IFACE_NONE (5) is "no value" (a void method's result). A STRING value never owns its text: an argument's text belongs to the caller for the duration of the call; a returned one (a property read, a method result) belongs to the widget and stays valid until the next call on that widget or until the widget changes or is destroyed -- copy it to keep it. wgtValueInt, wgtValueBool, wgtValueFloat, wgtValueDouble, and wgtValueStr (NULL becomes "") build values. FLOAT is BASIC's Single, DOUBLE its Double: a DOUBLE property or argument keeps every digit (a value through FLOAT is rounded to a float by a static widget's getter and setter, and the form runtime shows it as a Single); as text a DOUBLE has 15 significant digits.

    wgtValueToInt / wgtValueToBool / wgtValueToFloat / wgtValueToText

    int32_t     wgtValueToInt(const WgtValueT *v);
     bool        wgtValueToBool(const WgtValueT *v);
    @@ -5439,7 +5774,7 @@ const char *wgtValueToText(const WgtValueT *v, char *buf, int32_t bufSize);bool wgtParseBool(const char *text, bool defaultValue);

    The .frm and property-sheet boolean rule: "True" or "-1" is true, "False" or "0" false (case-insensitive), anything else (and NULL) defaultValue.

    Generic Widget Access

    -

    One implementation of "create, read, write and call by name" for every widget, static or dynamic. The form runtime, the IDE designer and C programs all use it; nothing else switches on WGT_SIG_* or WgtCreateSigE.

    +

    One implementation of "create, read, write, and call by name" for every widget, static or dynamic. The form runtime, the IDE designer, and C programs all use it; nothing else switches on WGT_SIG_* or WgtCreateSigE.

    wgtIfaceSetType / wgtGetIfaceOf

    void             wgtIfaceSetType(const char *name, int32_t typeId);
     const WgtIfaceT *wgtGetIfaceOf(const WidgetT *w, const char **outName);
    @@ -5482,7 +5817,7 @@ bool wgtIfaceSetPropText(const WgtIfaceT *iface, WidgetT *w, const WgtPropDescT

    wgtFireEvent

    bool wgtFireEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc);

    Fire event name on w. The universal events go to their WidgetT callback with their arguments taken from args (a missing one is 0): Click -> onClick, DblClick -> onDblClick, Change -> onChange, GotFocus -> onFocus, LostFocus -> onBlur, KeyPress (KeyAscii) -> onKeyPress, KeyDown / KeyUp (KeyCode, Shift) -> onKeyDown / onKeyUp, MouseDown / MouseUp / MouseMove (Button, X, Y, or the VB form Button, Shift, X, Y) -> onMouseDown / onMouseUp / onMouseMove (which take no Shift), Scroll (Delta) -> onScroll (names case-insensitive). Every other name goes to w->onEvent; the universal names never do, so a host that sets both sees each event once. Validate is not mapped (onValidate answers a question; fire it directly). onEvent may write back into args (ByRef parameters; the form runtime writes every ByRef parameter back, a String as text that stays valid until the next event). Returns false when a handler destroyed widgets -- do not touch w then.

    -

    ListView (ColumnClick), TextArea (GutterClick) and HelpView (LinkClick) fire their own events this way after their C callbacks (wgtListViewSetHeaderClickCallback, wgtTextAreaSetGutterClick, wgtHelpViewSetLinkCallback), which stay for C programs; the event is skipped when the C callback destroyed widgets. A TextArea's gutter takes clicks when either is set.

    +

    ListView (ColumnClick), TextArea (GutterClick), and HelpView (LinkClick) fire their own events this way after their C callbacks (wgtListViewSetHeaderClickCallback, wgtTextAreaSetGutterClick, wgtHelpViewSetLinkCallback), which stay for C programs; the event is skipped when the C callback destroyed widgets. A TextArea's gutter takes clicks when either is set.

    Property Data Type Constants

      Define              Value   Description
       ------              -----   -----------
    @@ -5647,14 +5982,14 @@ int32_t pick = dvxButtonBox(ctx, "Copy", "The disk is full."
     

    Color Dialog

    dvxColorDialog

    bool dvxColorDialog(AppContextT *ctx, const char *title, uint32_t *rgbInOut);
    -

    Display a modal color picker: a palette of 48 basic colors, Red, Green and Blue sliders each paired with a number box (0-255), and a preview swatch. Clicking a basic color sets all three channels; the basic color matching the current color is framed. From the keyboard, Tab moves between the sliders and number boxes (the Red slider has the focus first), the arrow keys, Page Up/Down, Home and End move a slider, and Enter chooses OK. Works at every color depth; on 8-bit desktops the swatches show the nearest palette colors, but the chosen value is exact.

    +

    Display a modal color picker: a palette of 48 basic colors, Red, Green, and Blue sliders each paired with a number box (0-255), and a preview swatch. Clicking a basic color sets all three channels; the basic color matching the current color is framed. From the keyboard, Tab moves between the sliders and number boxes (the Red slider has the focus first), the arrow keys, Page Up/Down, Home, and End move a slider, and Enter chooses OK. Works at every color depth; on 8-bit desktops the swatches show the nearest palette colors, but the chosen value is exact.

      Parameter     Description
       ---------     -----------
       ctx           Application context
       title         Window title (NULL: "Color")
       rgbInOut      In: the starting color; out: the chosen color. Both 0x00RRGGBB (DVX BASIC's colours are &HBBGGRR; wgtBasicColor converts between the two)

    Returns: true if OK was chosen, false if cancelled or closed (then *rgbInOut is unchanged).

    -

    Macros for the 0x00RRGGBB layout: DVX_RGB(r, g, b) builds a color from three 0-255 channels, and DVX_RGB_RED(rgb), DVX_RGB_GREEN(rgb) and DVX_RGB_BLUE(rgb) take one apart.

    +

    Macros for the 0x00RRGGBB layout: DVX_RGB(r, g, b) builds a color from three 0-255 channels, and DVX_RGB_RED(rgb), DVX_RGB_GREEN(rgb), and DVX_RGB_BLUE(rgb) take one apart.

    uint32_t rgb = DVX_RGB(r, g, b);
     
     if (dvxColorDialog(ctx, "Title Bar", &rgb)) {
    @@ -5669,7 +6004,7 @@ WidgetT *dvxDialogAddField(WindowT *dlg, const char *label, int32_t labelW, int3
     void     dvxDialogAddOkCancel(WindowT *dlg, const char *okLabel, DvxDialogOkFnT onOk, void *userCtx);
     void     dvxDialogEnd(WindowT *dlg, bool accepted);
     bool     dvxDialogRun(AppContextT *ctx, WindowT *dlg);
    -

    dvxDialogCreate makes an empty dialog contentW pixels wide (NULL on failure); add widgets under dlg->widgetRoot, or labelled text boxes with dvxDialogAddField. dvxDialogAddOkCancel adds the right-aligned OK (labelled okLabel, "&OK" when NULL) and Cancel buttons after them. dvxDialogRun fits the height to the widgets, centers the dialog, runs it and destroys it; it returns true when it ended with OK. Every created dialog must be run.

    +

    dvxDialogCreate makes an empty dialog contentW pixels wide (NULL on failure); add widgets under dlg->widgetRoot, or labelled text boxes with dvxDialogAddField. dvxDialogAddOkCancel adds the right-aligned OK (labelled okLabel, "&OK" when NULL) and Cancel buttons after them. dvxDialogRun fits the height to the widgets, centers the dialog, runs it, and destroys it; it returns true when it ended with OK. Every created dialog must be run.

    dvxDialogAddField adds a row: a label labelW pixels wide and a text box of up to maxLen characters holding value, filling the rest of the row. It returns the text box (NULL on failure); the box's parent is the row, so a Browse... button can be added after it with wgtButton(box->parent, ...). Give every field's label the same labelW so the boxes line up.

    onOk (may be NULL) runs when OK is chosen, while the widgets still exist: copy their values out and return true to close the dialog, or tell the user what is wrong and return false to keep it open. It is never called for Cancel.

    dvxDialogEnd ends a running dialog from any of the caller's own widget callbacks, such as a double-click in a list or an extra button: accepted true acts like OK (onOk runs first and can keep the dialog open), false like Cancel. Use it only on a dialog made by dvxDialogCreate.

    @@ -5691,6 +6026,22 @@ if (dvxDialogRun(ctx, dlg)) { DVX_SAVE_YES 1 Save, then proceed DVX_SAVE_NO 2 Discard, then proceed DVX_SAVE_CANCEL 3 Abort the operation
    +

    dvxFontDialog

    +
    bool dvxFontDialog(AppContextT *ctx, const char *title, uint32_t flags, DvxFontChoiceT *choiceInOut);
    +

    Display a modal font picker: lists of the font families (dvxText.h), the four styles (Regular, Bold, Italic, and Bold Italic), and the sizes the chosen family comes in for the chosen style; Underline and Strikeout boxes; and a sample drawn in the chosen font as it changes. Choosing another family or style keeps the size nearest the one chosen. The labels' underlined letters move to the lists from the keyboard; Enter chooses OK.

    +
      Parameter     Description
    +  ---------     -----------
    +  ctx           Application context
    +  title         Window title (NULL: "Font")
    +  flags         DVX_FONT_DLG_FIXED: offer only fixed-pitch families (for a text editor or a terminal)
    +  choiceInOut   In: the starting font (a family there is none of starts on "System"); out: the chosen one
    +

    DvxFontChoiceT holds family (FONT_FAMILY_MAX characters with the NUL), size (pixels), and style (FONT_STYLE_* bits, the lines included) -- what wgtSetFont and dvxFontOpen take.

    +

    Returns: true if OK was chosen, false if cancelled or closed (then *choiceInOut is unchanged).

    +
    DvxFontChoiceT choice = { "DejaVu Sans", 14, FONT_STYLE_BOLD };
    +
    +if (dvxFontDialog(ctx, NULL, 0, &choice)) {
    +    wgtSetFont(label, choice.family, choice.size, choice.style);
    +}

    dvxPromptSave

    int32_t dvxPromptSave(AppContextT *ctx, const char *title);

    Display a canonical "Save changes?" dialog with Yes/No/Cancel buttons and an appropriate question icon.

    @@ -5715,6 +6066,7 @@ if (dvxDialogRun(ctx, dlg)) { DVX_RES_ICON 1 Image data (BMP, PNG, etc.) DVX_RES_TEXT 2 Null-terminated string DVX_RES_BINARY 3 Arbitrary binary data (app-specific) + DVX_RES_FONT 4 A .DFN font the program carries (added for it when it loads; dvxText.h) DVX_RES_MAGIC 0x52585644 Footer signature ("DVXR" little-endian) DVX_RES_NAME_MAX 32 Max resource name length including null DVX_RES_ONLY_MAGIC "DVXRESONLY" First bytes (NUL included, DVX_RES_ONLY_MAGIC_LEN) of a file that holds only resources @@ -5800,7 +6152,7 @@ if (dvxDialogRun(ctx, dlg)) {

    dvxPrefs.h -- INI Preferences System

    Handle-based API over classic INI files. Each prefsLoad/prefsCreate returns a PrefsHandleT that must be passed to all subsequent calls and freed with prefsClose when done. Multiple INI files can be open simultaneously.

    -

    BASIC programs reach the same functions through prefs.bas (dvxPrefsBas.c in libdvx): PrefsLoad, PrefsCreate, PrefsGet$, PrefsGetInt, PrefsGetBool, PrefsSet, PrefsSetInt, PrefsSetBool, PrefsRemove, PrefsKeyName$ (prefsKeyName, "" past the last key), PrefsSave, PrefsSaveAs and PrefsClose, on handles (1, 2, ...; 0 = none) that belong to the program that made them and are closed, unsaved, when it ends. True is -1, False 0. PrefsDvxIni$ is DVX_INI_PATH, PrefsSetVideoMode is prefsSetVideoMode, and PrefsVideoWidth / PrefsVideoHeight / PrefsVideoBpp are the three values of prefsGetVideoMode (DVX_DEFAULT_VIDEO_* for a missing key or a bad handle). The choice tables below are PREFS_CHOICE_ACCEL (0), PREFS_CHOICE_WALLPAPER (1) and PREFS_CHOICE_WHEEL (2) there: PrefsChoiceCount, PrefsChoiceLabel$, PrefsChoiceName$ (iniName), PrefsChoiceValue, PrefsChoiceIndex, PrefsChoiceIndexOfValue, PrefsChoiceDefault (the index of the choice MOUSE_ACCEL_DEFAULT / WALLPAPER_MODE_DEFAULT / MOUSE_WHEEL_DIR_DEFAULT names; -1 for an unknown table), and PrefsGetChoice(handle, section, key, table) with the table's default. PrefsGetChoiceFrom(handle, section, key, choiceNames, defaultName) is prefsGetChoice on a table the program gives as "|"-separated names (each choice's iniName and label is its name, its value its index).

    +

    BASIC programs reach the same functions through prefs.bas (dvxPrefsBas.c in libdvx): PrefsLoad, PrefsCreate, PrefsGet$, PrefsGetInt, PrefsGetBool, PrefsSet, PrefsSetInt, PrefsSetBool, PrefsRemove, PrefsKeyName$ (prefsKeyName, "" past the last key), PrefsSave, PrefsSaveAs, and PrefsClose, on handles (1, 2, ...; 0 = none) that belong to the program that made them and are closed, unsaved, when it ends. True is -1, False 0. PrefsDvxIni$ is DVX_INI_PATH, PrefsSetVideoMode is prefsSetVideoMode, and PrefsVideoWidth / PrefsVideoHeight / PrefsVideoBpp are the three values of prefsGetVideoMode (DVX_DEFAULT_VIDEO_* for a missing key or a bad handle). The choice tables below are PREFS_CHOICE_ACCEL (0), PREFS_CHOICE_WALLPAPER (1), and PREFS_CHOICE_WHEEL (2) there: PrefsChoiceCount, PrefsChoiceLabel$, PrefsChoiceName$ (iniName), PrefsChoiceValue, PrefsChoiceIndex, PrefsChoiceIndexOfValue, PrefsChoiceDefault (the index of the choice MOUSE_ACCEL_DEFAULT / WALLPAPER_MODE_DEFAULT / MOUSE_WHEEL_DIR_DEFAULT names; -1 for an unknown table), and PrefsGetChoice(handle, section, key, table) with the table's default. PrefsGetChoiceFrom(handle, section, key, choiceNames, defaultName) is prefsGetChoice on a table the program gives as "|"-separated names (each choice's iniName and label is its name, its value its index).

    PrefsHandleT *prefsBasHandle(int32_t handle);

    The settings behind a prefs.bas handle, NULL for 0 or a handle that is not open. Other libraries' BASIC entry points that take a prefs.bas handle (system.bas, joystick.bas, net.bas, print.bas, sound.bas) resolve it with this; libdvx is loaded first, so the symbol is always there.

    Constants

    @@ -5824,8 +6176,8 @@ if (dvxDialogRun(ctx, dlg)) {
  • Lines of any length are read without truncation; LF and CR LF endings are accepted.
  • "[name]" starts a section; "key = value" is a key (blanks around key and value are trimmed). Keys before the first section belong to section "".
  • Section and key names match case-insensitively. With duplicates, the first one wins.
  • -
  • Blank lines, ';' and '#' comments and lines that are neither a section nor a key are kept.
  • -
  • On save, every line not changed through the API (comments, unknown lines, untouched keys and headers) keeps its content; changed or new keys are written as "key = value". Every line is written with CR LF endings, so a file with LF-only endings is rewritten in full.
  • +
  • Blank lines, ';' and '#' comments, and lines that are neither a section nor a key are kept.
  • +
  • On save, every line not changed through the API (comments, unknown lines, untouched keys, and headers) keeps its content; changed or new keys are written as "key = value". Every line is written with CR LF endings, so a file with LF-only endings is rewritten in full.
  •   Parameter   Description
       ---------   -----------
    @@ -5875,7 +6227,7 @@ if (dvxDialogRun(ctx, dlg)) {
     

    Returns: Parsed int, or defaultVal.

    prefsGetBool

    bool prefsGetBool(PrefsHandleT *h, const char *section, const char *key, bool defaultVal);
    -

    Retrieve a boolean value. The whole value must be "true", "yes" or "1" (true) or "false", "no" or "0" (false), in any case; anything else (e.g. "t", "10", "normal") returns defaultVal.

    +

    Retrieve a boolean value. The whole value must be "true", "yes", or "1" (true) or "false", "no", or "0" (false), in any case; anything else (e.g. "t", "10", "normal") returns defaultVal.

      Parameter    Description
       ---------    -----------
       h            Preferences handle
    @@ -5886,7 +6238,7 @@ if (dvxDialogRun(ctx, dlg)) {
     

    prefsGetVideoMode / prefsSetVideoMode

    void prefsGetVideoMode(PrefsHandleT *h, int32_t *width, int32_t *height, int32_t *bpp);
     void prefsSetVideoMode(PrefsHandleT *h, int32_t width, int32_t height, int32_t bpp);
    -

    Read or store the [video] display mode DVX starts in (keys width, height and bpp). Missing keys read as DVX_DEFAULT_VIDEO_W/H/BPP (640x480, 16 bpp). The one place those key names live: the shell (start-up and safe video mode), the Control Panel and Setup all go through these two functions.

    +

    Read or store the [video] display mode DVX starts in (keys width, height, and bpp). Missing keys read as DVX_DEFAULT_VIDEO_W/H/BPP (640x480, 16 bpp). The one place those key names live: the shell (start-up and safe video mode), the Control Panel, and Setup all go through these two functions.

    Setters

    prefsSetString

    void prefsSetString(PrefsHandleT *h, const char *section, const char *key, const char *value);
    @@ -5907,7 +6259,7 @@ void prefsSetVideoMode(PrefsHandleT *h, int32_t width, int32_t height, int32_t b printf("%s = %s\n", key, prefsGetString(h, "associations", key, "")); }

    Shared Preference Vocabularies

    -

    Enumerated preferences are stored in DVX.INI by name. One table per preference is the single source of truth for the label the Control Panel shows, the spelling written to the INI and the value applied.

    +

    Enumerated preferences are stored in DVX.INI by name. One table per preference is the single source of truth for the label the Control Panel shows, the spelling written to the INI, and the value applied.

    typedef struct {
         const char *label;    // user-facing text (dropdown item)
         const char *iniName;  // spelling stored in the INI
    @@ -5922,8 +6274,8 @@ extern const PrefsChoiceT prefsWheelDirChoices[PREFS_WHEEL_DIR_CHOICE_COUNT];
    -

    The entry counts are PREFS_ACCEL_CHOICE_COUNT (4), PREFS_WALLPAPER_CHOICE_COUNT (3) and PREFS_WHEEL_DIR_CHOICE_COUNT (2). MOUSE_WHEEL_DIR_NORMAL (1) and MOUSE_WHEEL_DIR_REVERSED (-1) in dvxTypes.h are also the wheelDir values for dvxSetMouseConfig.

    -

    Labels are the capitalized names ("Off", "Stretch", "Reversed", ...). The accel table is in dropdown order. dvxMouseFromPrefs and dvxWallpaperFromPrefs (dvxApp.h) read [mouse] wheel, [mouse] acceleration and [desktop] mode through prefsGetChoice, and the Control Panel builds its dropdowns from the same tables.

    +

    The entry counts are PREFS_ACCEL_CHOICE_COUNT (4), PREFS_WALLPAPER_CHOICE_COUNT (3), and PREFS_WHEEL_DIR_CHOICE_COUNT (2). MOUSE_WHEEL_DIR_NORMAL (1) and MOUSE_WHEEL_DIR_REVERSED (-1) in dvxTypes.h are also the wheelDir values for dvxSetMouseConfig.

    +

    Labels are the capitalized names ("Off", "Stretch", "Reversed", ...). The accel table is in dropdown order. dvxMouseFromPrefs and dvxWallpaperFromPrefs (dvxApp.h) read [mouse] wheel, [mouse] acceleration, and [desktop] mode through prefsGetChoice, and the Control Panel builds its dropdowns from the same tables.

    prefsChoiceIndex

    int32_t prefsChoiceIndex(const PrefsChoiceT *choices, int32_t count, const char *name);

    Return the index of the choice whose iniName equals name (case-insensitive), or -1 when there is none (also for a NULL name).

    @@ -6045,10 +6397,10 @@ VALIDATE_WIDGET_VOID(w, wtype); // returns void on mismatch

    Find the deepest widget at (x, y) within w's subtree. Respects WCLASS_NO_HIT_RECURSE: the search stops at such a container, whose class ON_MOUSE handler receives the press. Hit testing ignores the enabled flag; event dispatch checks widgetIsEnabledChain on the result.

    widgetCallbackTarget

    WidgetT *widgetCallbackTarget(WidgetT *root, int32_t x, int32_t y);
    -

    Find the widget the universal callbacks (onClick, onDblClick, onMouseDown/Up/Move), context menus, tooltips and the cursor shape go to. Starts from widgetHitTest; while the result is a non-focusable WCLASS_NO_HIT_RECURSE container (Splitter, ScrollPane), it continues into the child whose paintClip (the part actually visible when last painted) contains the point. Returns the container itself when the point is on its own chrome (divider, scrollbars) or no child is visible there.

    +

    Find the widget the universal callbacks (onClick, onDblClick, onMouseDown/Up/Move), context menus, tooltips, and the cursor shape go to. Starts from widgetHitTest; while the result is a non-focusable WCLASS_NO_HIT_RECURSE container (Splitter, ScrollPane), it continues into the child whose paintClip (the part actually visible when last painted) contains the point. Returns the container itself when the point is on its own chrome (divider, scrollbars) or no child is visible there.

    widgetClosePopup

    void widgetClosePopup(void);
    -

    Close the open dropdown/combobox popup, if any: dirty its overlay rect and fully invalidate its window so the overlay pixels are erased. The single close path for click-outside, clicks in another window, Escape and focus changes.

    +

    Close the open dropdown/combobox popup, if any: dirty its overlay rect and fully invalidate its window so the overlay pixels are erased. The single close path for click-outside, clicks in another window, Escape, and focus changes.

    widgetIsEnabledChain

    bool widgetIsEnabledChain(const WidgetT *w);

    True when w and every ancestor are enabled. Disabling a container disables input to its whole subtree: mouse and keyboard dispatch use this test.

    @@ -6106,7 +6458,7 @@ VALIDATE_WIDGET_VOID(w, wtype); // returns void on mismatch

    Map a dragged thumb position to a scroll position in [0, maxScroll]. relMouse is where the thumb's leading edge should be, relative to the start of a track of trackLen pixels (the mouse position minus the grab offset). Returns 0 when maxScroll <= 0.

    widgetScrollbarNeeds

    void widgetScrollbarNeeds(int32_t totalW, int32_t totalH, int32_t *innerW, int32_t *innerH, bool *needV, bool *needH);
    -

    Which scrollbars content totalW x totalH pixels needs in a viewport of *innerW x *innerH (the area inside the widget's border). A vertical bar narrows the viewport, which may make the content too wide; a horizontal bar shortens it, which may make the content too tall; both cases are followed through. *innerW and *innerH come back less the bars (WGT_SB_W each). For rows of a fixed height pass rows * height: the content is too tall exactly when there are more rows than whole rows fit. ListBox, ListView, TreeView, ScrollPane and DBGrid decide their bars with it; BASIC's ScrollBarsNeeded is the same rule.

    +

    Which scrollbars content totalW x totalH pixels needs in a viewport of *innerW x *innerH (the area inside the widget's border). A vertical bar narrows the viewport, which may make the content too wide; a horizontal bar shortens it, which may make the content too tall; both cases are followed through. *innerW and *innerH come back less the bars (WGT_SB_W each). For rows of a fixed height pass rows * height: the content is too tall exactly when there are more rows than whole rows fit. ListBox, ListView, TreeView, ScrollPane, and DBGrid decide their bars with it; BASIC's ScrollBarsNeeded is the same rule.

    Layout Functions

    widgetCalcMinSizeBox / widgetCalcMinSizeTree

    void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font);
    @@ -6137,6 +6489,9 @@ void widgetOnBlur(WindowT *win);

    widgetPaintOverlays

    void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);

    Paint the overlay pass: dropdown popups and tooltips drawn on top of the main tree.

    +

    widgetQuickPaintBegin

    +
    bool widgetQuickPaintBegin(WidgetT *w, DisplayT *cd);
    +

    A display onto w's window content buffer, clipped to w and every ancestor, for a widget's quick repaint (WGT_METHOD_QUICK_REPAINT) outside the paint walk: the widget draws the rows that changed and reports them, and only those are flushed. Returns false when w cannot be painted that way now -- hidden, clipped away entirely, or inside a scroll container, whose viewport is not its rect (w is then invalidated for the normal paint) -- and the caller drops what it meant to draw. The Terminal and RichEdit widgets use it.

    Pressable Button Helpers

    Shared state machine for WCLASS_PRESS_RELEASE widgets (Button, ImageButton). The w->pressed flag tracks the visual state; these helpers fire the widget's onClick callback on release-within-bounds.

    widgetPressableOnMouse / widgetPressableOnKey / widgetPressableOnDragUpdate / widgetPressableOnDragEnd / widgetPressableOnAccelActivate

    @@ -6189,7 +6544,7 @@ void widgetTextSet(WidgetT *w, const char *text);

    Append a formatted line to dvx.log. Implemented in dvx.exe and exported to every DXE via the loader.

    platformInit

    void platformInit(void);
    -

    One-time platform initialization (a second call does nothing new; the loader and dvxInit both call it). On DOS this disables Ctrl+C and Ctrl+Break at every level (the DJGPP runtime check, SIGINT, and DOS break checking via INT 21h AH=33h) so the user cannot kill the GUI from inside a graphics mode, and points the real-mode INT 24h (critical error) vector at a stub in DOS memory that answers Fail. From then on a drive that is not ready, a write-protected disk or a removed disk makes the DOS call return an error (errno) instead of waiting at "Abort, Retry, Fail" on the hidden text screen, so no DVX code needs a guard of its own. The handler is never removed: DOS restores the vector from the PSP when DVX exits. It does not install crash handlers (see platformInstallCrashHandler).

    +

    One-time platform initialization (a second call does nothing new; the loader and dvxInit both call it). On DOS this disables Ctrl+C and Ctrl+Break at every level (the DJGPP runtime check, SIGINT, and DOS break checking via INT 21h AH=33h) so the user cannot kill the GUI from inside a graphics mode, and points the real-mode INT 24h (critical error) vector at a stub in DOS memory that answers Fail. From then on a drive that is not ready, a write-protected disk, or a removed disk makes the DOS call return an error (errno) instead of waiting at "Abort, Retry, Fail" on the hidden text screen, so no DVX code needs a guard of its own. The handler is never removed: DOS restores the vector from the PSP when DVX exits. It does not install crash handlers (see platformInstallCrashHandler).

    platformYield

    void platformYield(void);

    Cooperative CPU yield, used between frames when the event loop has nothing to do. On DOS this calls __dpmi_yield() to be friendly to multitaskers.

    @@ -6224,11 +6579,11 @@ int32_t platformTimerAddFast(uint32_t hz, PlatformTimerFnT fn, void *userData, i void platformTimerRemove(int32_t timerId); void platformTimerRemoveOwner(int32_t ownerAppId); void platformTimerStats(PlatformTimerStatsT *out); -

    One timer interrupt for everything that must run at a steady rate however busy the programs are (audio refill, joystick polling). dvxInit starts the service; dvxShutdown stops it. On DOS it owns PIT channel 0 (rate generator mode) and INT 8: the interrupt runs as fast as the fastest registered timer needs (at most PLATFORM_TIMER_MAX_HZ; the BIOS 18.2 Hz when there is none) and exactly one interrupt per 65536 PIT clocks is passed on to the BIOS handler, so the BIOS tick count, the DOS time of day, clock() and the floppy motor timeout keep their normal rate. A rate change takes effect at once (the PIT is restarted and the part of the period already run is counted). Shutdown restores the BIOS square-wave mode and the old vector; it also runs at exit() and from the crash handler when a crash is not recovered.

    +

    One timer interrupt for everything that must run at a steady rate however busy the programs are (audio refill, joystick polling). dvxInit starts the service; dvxShutdown stops it. On DOS it owns PIT channel 0 (rate generator mode) and INT 8: the interrupt runs as fast as the fastest registered timer needs (at most PLATFORM_TIMER_MAX_HZ; the BIOS 18.2 Hz when there is none) and exactly one interrupt per 65536 PIT clocks is passed on to the BIOS handler, so the BIOS tick count, the DOS time of day, clock(), and the floppy motor timeout keep their normal rate. A rate change takes effect at once (the PIT is restarted and the part of the period already run is counted). Shutdown restores the BIOS square-wave mode and the old vector; it also runs at exit() and from the crash handler when a crash is not recovered.

    platformTimerAdd calls fn(userData) hz times a second, 1 to PLATFORM_TIMER_MAX_HZ; exact on average, while single intervals vary by up to one interrupt period. It returns a timer id, or -1 (bad arguments, all PLATFORM_TIMER_SLOTS in use, service not running). ownerAppId is recorded for platformTimerRemoveOwner, which the shell calls when a program ends; use 0 for a library that removes its own timers. gameTimerAdd (game.lib) registers one for the calling program. After platformTimerRemove returns, the callback is not running and will not run again.

    platformTimerAddFast is the same for a device that must be fed at an audio rate (the PC speaker's digital sound): hz may be up to PLATFORM_TIMER_FAST_MAX_HZ. The whole timer interrupt then runs that often, so the callback must be very short (a few port writes); ask for PLATFORM_PIT_HZ / divisor to be called on every interrupt.

    -
    Warning: The callback runs INSIDE THE TIMER INTERRUPT, with interrupts disabled, on a small stack. Keep it to a few dozen microseconds; do not call the C library, DVX or DOS; do not use floating point; and pin its code and every byte it touches with platformLockRegion (a page fault there is fatal on DOS). Set flags and counters, fill buffers, write I/O ports.
    -

    PlatformTimerStatsT reports rateHz (the interrupt rate, rounded), divisor (the PIT reload value), interrupts (since the service started) and biosTicks (of those, passed on to the BIOS).

    +
    Warning: The callback runs INSIDE THE TIMER INTERRUPT, with interrupts disabled, on a small stack. Keep it to a few dozen microseconds; do not call the C library, DVX, or DOS; do not use floating point; and pin its code and every byte it touches with platformLockRegion (a page fault there is fatal on DOS). Set flags and counters, fill buffers, write I/O ports.
    +

    PlatformTimerStatsT reports rateHz (the interrupt rate, rounded), divisor (the PIT reload value), interrupts (since the service started), and biosTicks (of those, passed on to the BIOS).

    Interrupt-Safe Updates and Shutdown Hooks

    uint32_t platformIrqSave(void);
     void     platformIrqRestore(uint32_t state);
    @@ -6247,7 +6602,7 @@ void    platformVideoShutdown(DisplayT *d);

    Low-level video mode setup and teardown. Called by videoInit / videoShutdown.

    platformVideoEnumModes

    void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *userData), void *userData);
    -

    Enumerate the graphics modes platformVideoInit can actually set (on DOS: VBE modes with a linear framebuffer at 8, 15, 16 or 32 bpp; 24 bpp is not offered). cb is invoked once per distinct width/height/depth. Feeds the video-mode list offered to the user; platformVideoInit selects its own best match independently.

    +

    Enumerate the graphics modes platformVideoInit can actually set (on DOS: VBE modes with a linear framebuffer at 8, 15, 16, or 32 bpp; 24 bpp is not offered). cb is invoked once per distinct width/height/depth. Feeds the video-mode list offered to the user; platformVideoInit selects its own best match independently.

    platformVideoSetPalette

    void platformVideoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t count);

    Program the VGA/VESA DAC palette registers (8-bit mode only). pal is the full 256-entry RGB table (3 bytes per entry); entries firstEntry..firstEntry+count-1 are written, clamped to the 256-entry table.

    @@ -6272,7 +6627,7 @@ void platformSpanCopy32(uint8_t *dst, const uint8_t *src, int32_t count);

    Mouse Input

    platformMouseInit

    void platformMouseInit(int32_t screenW, int32_t screenH);
    -

    Initialize the mouse driver and constrain movement to the screen bounds. On DOS this resets the INT 33h driver, sets the X/Y range and centers the cursor. Safe to call again after a video-mode change: the speed, acceleration and wheel settings applied earlier are re-applied after the driver reset.

    +

    Initialize the mouse driver and constrain movement to the screen bounds. On DOS this resets the INT 33h driver, sets the X/Y range, and centers the cursor. Safe to call again after a video-mode change: the speed, acceleration, and wheel settings applied earlier are re-applied after the driver reset.

    platformMousePoll

    void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons);

    Poll current mouse state. Buttons is a bitmask: bit 0 = left, bit 1 = right, bit 2 = middle. MOUSE_BUTTON_MASK (MOUSE_LEFT | MOUSE_RIGHT | MOUSE_MIDDLE, in dvxPlat.h) covers every bit it can report.

    @@ -6307,7 +6662,7 @@ bool platformKeyIsDown(int32_t rawCode); void platformKeyStateGet(uint8_t out[KEY_RAW_COUNT]); void platformKeyStateClear(void); void platformInjectScan(uint8_t scan); -

    The keyboard hook (on DOS a chain on INT 9 that reads port 60h before the BIOS handler stores the key for platformKeyboardRead) decodes every set-1 byte: 0xE0 prefixes, the fake Shift codes the keyboard wraps around grey keys (dropped), the Pause burst (one press and one release) and keyboard replies such as ACK and overrun (dropped). Each key going down or up updates the held-key table and is queued as a RawKeyEventT (at most PLATFORM_KEY_EVENT_QUEUE; newer events are dropped when the queue is full, the table still changes). Keys are named by raw code, KEY_RAW_CODE(scancode, extended) (see dvxTypes.h): Left and Right Shift, Ctrl and Alt are keys of their own, and the code does not change with modifiers. dvxInit installs the hook and dvxShutdown removes it; the event loop reads the queue (onKeyUp, onRawKey) and clears the table whenever the keyboard focus moves. platformKeyStateGet copies the table (1 = down). platformInjectScan feeds one byte to the decoder as if the keyboard had sent it (the test script's KEYDOWN / KEYUP use it).

    +

    The keyboard hook (on DOS a chain on INT 9 that reads port 60h before the BIOS handler stores the key for platformKeyboardRead) decodes every set-1 byte: 0xE0 prefixes, the fake Shift codes the keyboard wraps around grey keys (dropped), the Pause burst (one press and one release), and keyboard replies such as ACK and overrun (dropped). Each key going down or up updates the held-key table and is queued as a RawKeyEventT (at most PLATFORM_KEY_EVENT_QUEUE; newer events are dropped when the queue is full, the table still changes). Keys are named by raw code, KEY_RAW_CODE(scancode, extended) (see dvxTypes.h): Left and Right Shift, Ctrl, and Alt are keys of their own, and the code does not change with modifiers. dvxInit installs the hook and dvxShutdown removes it; the event loop reads the queue (onKeyUp, onRawKey) and clears the table whenever the keyboard focus moves. platformKeyStateGet copies the table (1 = down). platformInjectScan feeds one byte to the decoder as if the keyboard had sent it (the test script's KEYDOWN / KEYUP use it).

    platformAltScanToChar

    char platformAltScanToChar(int32_t scancode);

    Translate an Alt+key scancode to its corresponding ASCII character. Returns 0 for non-printable scancodes.

    @@ -6321,7 +6676,7 @@ void platformInjectScan(uint8_t scan);

    File System

    platformValidateFilename

    const char *platformValidateFilename(const char *dir, const char *name);
    -

    Validate a name for a new file or folder in folder dir (NULL or "" for the current one) against the rules of its disk. On DOS: platformValidateLongName where platformLongNames(dir), else platformValidateShortName, and dir with the name must stay under DVX_MAX_PATH. The host backend refuses an empty name, . and .., separators and control characters, and ignores dir. Returns NULL if valid, or a human-readable error string. The file dialog, the File Manager (fileOpsValidateName) and the IDE's project rename all use it.

    +

    Validate a name for a new file or folder in folder dir (NULL or "" for the current one) against the rules of its disk. On DOS: platformValidateLongName where platformLongNames(dir), else platformValidateShortName, and dir with the name must stay under DVX_MAX_PATH. The host backend refuses an empty name, . and .., separators, and control characters, and ignores dir. Returns NULL if valid, or a human-readable error string. The file dialog, the File Manager (fileOpsValidateName), and the IDE's project rename all use it.

    Long file names: platformLongNames / platformValidateLongName / platformValidateShortName / platformLongNameChar / platformShortPath

    #define PLATFORM_LONG_NAME_MAX 255
     
    @@ -6330,9 +6685,9 @@ const char *platformValidateLongName(const char *name);
     const char *platformValidateShortName(const char *name);
     bool        platformLongNameChar(char c);
     bool        platformShortPath(const char *path, char *out, int32_t outSize);
    -

    The DOS C library uses the long-name calls (INT 21h 71xxh) by itself whenever a driver provides them (Windows, DOSLFN), so open, fopen, rename, mkdir, stat and the directory readers all take and return long names; these functions cover what the C library does not.

    +

    The DOS C library uses the long-name calls (INT 21h 71xxh) by itself whenever a driver provides them (Windows, DOSLFN), so open, fopen, rename, mkdir, stat, and the directory readers all take and return long names; these functions cover what the C library does not.

    platformLongNames is true when the drive holding path (NULL or "": the current drive) takes long file names. On DOS that is a loaded long-name driver covering that drive; elsewhere always.

    -

    platformValidateLongName and platformValidateShortName are the two DOS name rules by themselves, whatever the disk takes; each returns NULL or why the name is not valid. A long name has 1 to PLATFORM_LONG_NAME_MAX characters and may hold spaces, several dots and + , ; = [ ]; it may not hold a control character or \ / : * ? " < > |, and may not end with a dot or a space (DOS drops them). An 8.3 name has 1 to 8 printable ASCII characters, then at most one dot and up to 3 more, none of them a space or " * + , / : ; < = > ? [ \ ] |. Neither may be . or .., or a device name (CON, PRN, AUX, NUL, CLOCK$, COM1 to COM9, LPT1 to LPT9) before its first dot. They are shared code, so the host tests check the DOS rules too.

    +

    platformValidateLongName and platformValidateShortName are the two DOS name rules by themselves, whatever the disk takes; each returns NULL or why the name is not valid. A long name has 1 to PLATFORM_LONG_NAME_MAX characters and may hold spaces, several dots, and + , ; = [ ]; it may not hold a control character or \ / : * ? " < > |, and may not end with a dot or a space (DOS drops them). An 8.3 name has 1 to 8 printable ASCII characters, then at most one dot and up to 3 more, none of them a space or " * + , / : ; < = > ? [ \ ] |. Neither may be . or .., or a device name (CON, PRN, AUX, NUL, CLOCK$, COM1 to COM9, LPT1 to LPT9) before its first dot. They are shared code, so the host tests check the DOS rules too.

    platformLongNameChar is true when a long name may hold character c; a file transfer uses it to turn the characters a sender's name cannot keep into _.

    platformShortPath writes the short (8.3) form of an existing path into out, for what reads names before a long-name driver loads (AUTOEXEC.BAT) or cannot take blanks (an old program's command line). On a drive with long names DOS gives every part's short name (INT 21h AX=7160h CL=1); elsewhere, and when DOS cannot, path itself is copied (platformPathTidy: "C:/DVX" gives "C:\\DVX"). False when out is too small.

    platformStatSize

    @@ -6352,7 +6707,7 @@ const char *platformPathIsUnder(const char *path, const char *dir); bool platformPathJoin(char *out, int32_t outSize, const char *dir, const char *name); bool platformPathDirName(const char *path, char *out, int32_t outSize); char *platformPathTidy(char *path); -

    DVX paths follow DOS rules on every platform: '/' and '\\' are the same separator, so a path a program writes with '/' works on DOS exactly as with '\\', and names compare without case (DOS and the host build also share INI files, projects and program paths). Every path DVX shows uses the native separator, DVX_PATH_SEP ('\\' on DOS). DVX's own path code goes through these functions; a program that splits, compares or joins paths should too, never testing for one separator by hand.

    +

    DVX paths follow DOS rules on every platform: '/' and '\\' are the same separator, so a path a program writes with '/' works on DOS exactly as with '\\', and names compare without case (DOS and the host build also share INI files, projects, and program paths). Every path DVX shows uses the native separator, DVX_PATH_SEP ('\\' on DOS). DVX's own path code goes through these functions; a program that splits, compares, or joins paths should too, never testing for one separator by hand.

    • platformPathIsSep: true for '/' and '\\'.
    • platformPathNative: rewrites path's separators as DVX_PATH_SEP, in place, before it is shown (the DOS C library's getcwd and realpath give "c:/dvx"). Returns path.
    • @@ -6383,7 +6738,7 @@ bool platformPathSibling(char *out, int32_t outSize, const char *path, const cha

      Write path with its file-name extension replaced by ext into out, followed by as many leading characters of the original extension as fit in PLATFORM_SIBLING_EXT_MAX characters (a full-length ext such as ".BAK" replaces the extension outright; a name with no extension gets ext appended). Replacing rather than appending keeps an 8.3 name valid on DOS without long file names, and keeping part of the original extension gives files that differ only in extension distinct scratch names: CONFIG\DVX.INI becomes CONFIG\DVX.$IN (temp) and CONFIG\DVX.~IN (backup), FOO.APP and FOO.HLP become FOO.$AP and FOO.$HL. Returns false when the result does not fit in outSize or would name path itself (compared case-insensitively).

      platformReplaceFile

      bool platformReplaceFile(const char *tmpPath, const char *path, const char *bakPath);
      -

      Crash-safe swap of a fully written tmpPath in for path. The original is renamed aside to bakPath first (DOS rename cannot overwrite) and removed only once the replacement is in place; if the swap fails the original is restored and tmpPath removed. A directory at path is never moved aside: the call fails and removes tmpPath. A leftover bakPath is removed only while path exists; with path missing it may be the only copy left by a crash between the two renames, so it is kept (prefsLoad reads it back). The preferences system, the resource writer and the help compiler all save this way, with the PLATFORM_TMP_EXT and PLATFORM_BAK_EXT siblings from platformPathSibling.

      +

      Crash-safe swap of a fully written tmpPath in for path. The original is renamed aside to bakPath first (DOS rename cannot overwrite) and removed only once the replacement is in place; if the swap fails the original is restored and tmpPath removed. A directory at path is never moved aside: the call fails and removes tmpPath. A leftover bakPath is removed only while path exists; with path missing it may be the only copy left by a crash between the two renames, so it is kept (prefsLoad reads it back). The preferences system, the resource writer, and the help compiler all save this way, with the PLATFORM_TMP_EXT and PLATFORM_BAK_EXT siblings from platformPathSibling.

      platformReadFile

      char *platformReadFile(const char *path, int32_t *outLen);

      Slurp a whole file into a freshly malloc'd, NUL-terminated buffer. Caller frees. Binary-safe: the NUL is past the end of the reported length.

      @@ -6393,7 +6748,7 @@ bool platformPathSibling(char *out, int32_t outSize, const char *path, const cha

      platformFilterMatch

      #define PLATFORM_FILTER_ALL "*.*"
       bool platformFilterMatch(const char *name, const char *filter);
      -

      True when name matches filter: platformGlobMatch patterns separated by ';', ',' or blanks ("*.txt;*.bat", "*.bmp, *.jpg"). A pattern equal to PLATFORM_FILTER_ALL matches every file, with or without an extension, as on DOS; a NULL or blank filter matches everything too. The file dialog (dvxFileDialog) and the file operations library (fileOpsListDir) both use it, so a filter behaves the same everywhere.

      +

      True when name matches filter: platformGlobMatch patterns separated by ';', ',', or blanks ("*.txt;*.bat", "*.bmp, *.jpg"). A pattern equal to PLATFORM_FILTER_ALL matches every file, with or without an extension, as on DOS; a NULL or blank filter matches everything too. The file dialog (dvxFileDialog) and the file operations library (fileOpsListDir) both use it, so a filter behaves the same everywhere.

      platformDriveList / platformDiskSpace / platformBootDrive

      #define PLATFORM_DRIVE_MAX      26
       #define PLATFORM_DRIVE_ROOT_MAX 4
      @@ -6443,7 +6798,7 @@ int32_t     platformStripLineEndings(char *buf, int32_t len);
      #define DVX_ARG_QUOTE '"'
       
       const char *dvxNextArg(const char *p, char *out, int32_t outSize, bool *quoted);
      -

      Copy the next argument of the command line p into out and return the text after it, or NULL when only blanks are left (or p is NULL). Arguments are separated by blanks and tabs; one that starts with a double quote runs to the closing quote (or the end), keeps its blanks and loses the quotes, and *quoted is set. The copy is truncated to fit outSize. This is the one rule for splitting command lines: the shell's Run command, program launch arguments and the BASIC COMMAND$ function all use it. Walk every argument with:

      +

      Copy the next argument of the command line p into out and return the text after it, or NULL when only blanks are left (or p is NULL). Arguments are separated by blanks and tabs; one that starts with a double quote runs to the closing quote (or the end), keeps its blanks, and loses the quotes, and *quoted is set. The copy is truncated to fit outSize. This is the one rule for splitting command lines: the shell's Run command, program launch arguments, and the BASIC COMMAND$ function all use it. Walk every argument with:

      const char *p = args;
       bool        quoted;
       char        arg[DVX_MAX_PATH];
      @@ -6483,7 +6838,7 @@ void platformAppAbort(void);

      Log platform-specific crash diagnostics. Called internally from the crash handler and exposed for manual invocation.

      platformOutOfMemory

      void platformOutOfMemory(size_t bytes);
      -

      Out-of-memory hook for the stb_ds dynamic arrays and hash maps (arrput, hmput, shput and friends have no error return). stb_ds_wrap.h points STBDS_OUT_OF_MEMORY at it wherever the stb_ds implementation is compiled. It logs "out of memory (N bytes) in dynamic array" via dvxLog and raises SIGSEGV, so under platformInstallCrashHandler a failed growth takes the normal crash-recovery path: the shell terminates the app that was current (its task, or its callback running on the shell task), and a failure with no app current is logged and the shell returns to its main loop. With no handler installed (the loader, the tools, the BASIC compiler) the process terminates. It never returns. The stb_ds patch that adds the hook is recorded in thirdparty/stb_ds.PATCHES.md.

      +

      Out-of-memory hook for the stb_ds dynamic arrays and hash maps (arrput, hmput, shput, and friends have no error return). stb_ds_wrap.h points STBDS_OUT_OF_MEMORY at it wherever the stb_ds implementation is compiled. It logs "out of memory (N bytes) in dynamic array" via dvxLog and raises SIGSEGV, so under platformInstallCrashHandler a failed growth takes the normal crash-recovery path: the shell terminates the app that was current (its task, or its callback running on the shell task), and a failure with no app current is logged and the shell returns to its main loop. With no handler installed (the loader, the tools, the BASIC compiler) the process terminates. It never returns. The stb_ds patch that adds the hook is recorded in thirdparty/stb_ds.PATCHES.md.

      VGA Splash Screen

      platformSplashInit / platformSplashShutdown

      void platformSplashInit(void);
      @@ -6510,7 +6865,7 @@ void platformSplashShutdown(void);

    Sound Library

    -

    Sound for DVX programs through whatever sound hardware the computer has: sampled sound from WAV and VOC files, tones and noise, and streams a program feeds itself (music, speech). Every program opens its own audio context; any number of programs play at the same time through one mixer, and everything a program opened is stopped and released when it ends, normally, killed or crashed.

    +

    Sound for DVX programs through whatever sound hardware the computer has: sampled sound from WAV and VOC files, tones and noise, and streams a program feeds itself (music, speech). Every program opens its own audio context; any number of programs play at the same time through one mixer, and everything a program opened is stopped and released when it ends, normally, killed, or crashed.

    Header: audio/audio.h (programs), audio/audioDrv.h (sound drivers)

    Loaded as: bin/libs/kpunch/audio/audio.lib, drivers in bin/libs/kpunch/audio/drivers/*.drv

    Contexts and the Device

    @@ -6523,19 +6878,19 @@ if (voice < 0) { } ... audioClose(audio); // or let the program end -

    The first audioOpen starts the sound device and the last audioClose stops it. In between, after AUDIO_IDLE_CLOSE_SEC (3) seconds with nothing playing, the device's output is closed so it costs no CPU time (desktop sounds would otherwise keep it mixing silence), and the next sound opens it again with the same driver before it starts, so nothing of it is lost; audioGetInfo still reports the device. Functions returning int32_t give AUDIO_OK or a voice id (above 0) on success and a negative AUDIO_ERR_* code on failure; audioErrorText says what a code means. audioGetInfo describes the device: the driver's name and description, the mixing rate it achieved, its output bits and channels, the voice limit, and whether it plays sampled sound (hasDac), has a hardware synthesizer (hasSynth) or plays tones on a hardware tone generator (hasTone, the PC speaker). audioGetStats counts mixed blocks and frames, stream underruns, clipped samples, dropped voices and the voices playing.

    +

    The first audioOpen starts the sound device and the last audioClose stops it. In between, after AUDIO_IDLE_CLOSE_SEC (3) seconds with nothing playing, the device's output is closed so it costs no CPU time (desktop sounds would otherwise keep it mixing silence), and the next sound opens it again with the same driver before it starts, so nothing of it is lost; audioGetInfo still reports the device. Functions returning int32_t give AUDIO_OK or a voice id (above 0) on success and a negative AUDIO_ERR_* code on failure; audioErrorText says what a code means. audioGetInfo describes the device: the driver's name and description, the mixing rate it achieved, its output bits and channels, the voice limit, and whether it plays sampled sound (hasDac), has a hardware synthesizer (hasSynth), or plays tones on a hardware tone generator (hasTone, the PC speaker). audioGetStats counts mixed blocks and frames, stream underruns, clipped samples, dropped voices, and the voices playing.

    None of the functions may be called from an interrupt (a timer-service callback).

    Samples and Voices

    -

    audioSampleLoad (file), audioSampleLoadMem (memory) and audioSampleCreate (raw PCM in an AudioFormatT: rate, 8 bits unsigned or 16 bits signed, 1 or 2 channels) make an AudioSampleT; audioSampleFree frees it and stops the voices playing it. audioPlay(sample, volume, pan, loop) plays it on a new voice; audioPlayFile loads a file and plays it, freeing the sample when the voice ends.

    +

    audioSampleLoad (file), audioSampleLoadMem (memory), and audioSampleCreate (raw PCM in an AudioFormatT: rate, 8 bits unsigned or 16 bits signed, 1 or 2 channels) make an AudioSampleT; audioSampleFree frees it and stops the voices playing it. audioPlay(sample, volume, pan, loop) plays it on a new voice; audioPlayFile loads a file and plays it, freeing the sample when the voice ends.

      Setting              Range
       -------              -----
       volume               0 .. AUDIO_VOLUME_MAX (256)
       pan                  AUDIO_PAN_LEFT (-256) .. AUDIO_PAN_RIGHT (256); centre plays at full volume on both sides and panning turns the other side down
       pitch                AUDIO_PITCH_MIN (64) .. AUDIO_PITCH_MAX (1024), AUDIO_PITCH_NORMAL (256) = recorded speed
    -

    audioVoiceSetVolume, audioVoiceSetPan, audioVoiceSetPitch, audioVoiceStop and audioVoiceIsPlaying take the context and the voice id; a voice id of another context, or of a voice that has ended, gives AUDIO_ERR_VOICE. audioStopAll stops every sample and tone of a context.

    -

    audioSetEndFn(ctx, fn, userData) asks for a call when a voice of ctx ends, with the reason: AUDIO_END_FINISHED (played out), AUDIO_END_STOPPED (stopped, or its sample or stream went away) or AUDIO_END_DROPPED (the voice limit made room for a louder sound). The call comes from audioPump or the shell's idle time, as the program that owns the context, never from an interrupt. The mixer runs up to one buffer (about 50 ms) ahead of what is heard, so a notification can come that much before the sound has finished playing.

    +

    audioVoiceSetVolume, audioVoiceSetPan, audioVoiceSetPitch, audioVoiceStop, and audioVoiceIsPlaying take the context and the voice id; a voice id of another context, or of a voice that has ended, gives AUDIO_ERR_VOICE. audioStopAll stops every sample and tone of a context.

    +

    audioSetEndFn(ctx, fn, userData) asks for a call when a voice of ctx ends, with the reason: AUDIO_END_FINISHED (played out), AUDIO_END_STOPPED (stopped, or its sample or stream went away), or AUDIO_END_DROPPED (the voice limit made room for a louder sound). The call comes from audioPump or the shell's idle time, as the program that owns the context, never from an interrupt. The mixer runs up to one buffer (about 50 ms) ahead of what is heard, so a notification can come that much before the sound has finished playing.

    Tones

    -

    audioTone(ctx, wave, hz, volume, ms) plays a square (AUDIO_WAVE_SQUARE), triangle (AUDIO_WAVE_TRIANGLE) or noise (AUDIO_WAVE_NOISE) tone of hz (AUDIO_TONE_MIN_HZ 20 to AUDIO_TONE_MAX_HZ 20000) for ms milliseconds, or until stopped when ms is 0. For noise, hz is how fast the noise changes: higher sounds brighter. audioVoiceSetFrequency changes a playing tone. When the computer has an FM chip (and DVX.INI [audio] fmTones is not false), a square or triangle tone plays on a free FM voice (a square as a carrier modulated at twice its pitch, which gives odd harmonics like a square wave; a triangle as a sine) at up to the chip's highest note (about 6,200 Hz); when every FM voice is taken it is mixed like any sound. Its voice id, volume, pan, pitch and end notification work the same either way. On a device with a DAC tones are mixed like any sound; on the PC speaker in its tones mode the loudest square or triangle tone playing is sounded by the timer chip.

    +

    audioTone(ctx, wave, hz, volume, ms) plays a square (AUDIO_WAVE_SQUARE), triangle (AUDIO_WAVE_TRIANGLE), or noise (AUDIO_WAVE_NOISE) tone of hz (AUDIO_TONE_MIN_HZ 20 to AUDIO_TONE_MAX_HZ 20000) for ms milliseconds, or until stopped when ms is 0. For noise, hz is how fast the noise changes: higher sounds brighter. audioVoiceSetFrequency changes a playing tone. When the computer has an FM chip (and DVX.INI [audio] fmTones is not false), a square or triangle tone plays on a free FM voice (a square as a carrier modulated at twice its pitch, which gives odd harmonics like a square wave; a triangle as a sine) at up to the chip's highest note (about 6,200 Hz); when every FM voice is taken it is mixed like any sound. Its voice id, volume, pan, pitch, and end notification work the same either way. On a device with a DAC tones are mixed like any sound; on the PC speaker in its tones mode the loudest square or triangle tone playing is sounded by the timer chip.

    Note Queues

    AudioNotesT *tune = NULL;
     
    @@ -6556,25 +6911,25 @@ audioFmWrite(audio, v, AUDIO_FM_CAR_LEVEL, 0);
     audioFmWrite(audio, v, AUDIO_FM_FNUM_LOW, fnum & 0xFF);
     audioFmWrite(audio, v, AUDIO_FM_KEY_BLOCK, AUDIO_FM_KEY_ON | (block << AUDIO_FM_BLOCK_SHIFT) | (fnum >> AUDIO_FM_FNUM_HI_SHIFT));
     audioFmFree(audio, v);                          // key off, back to everyone
    -

    An OPL2 (9 voices) or OPL3 (18 voices, each playing left, right or both) found by the opl driver is shared by every program: audioFmAlloc(ctx) takes a two-operator voice (the least recently given back first, so a released note's tail is cut last) and returns its id, AUDIO_ERR_NO_VOICE when all are taken, AUDIO_ERR_BUSY while a program has the whole chip, AUDIO_ERR_NO_SYNTH without a chip. audioFmWrite(ctx, voice, reg, value) sets a register of the voice's channel, numbered as for the first channel of an OPL2 (AUDIO_FM_MOD_CHAR 20h, AUDIO_FM_CAR_CHAR 23h, levels 40h/43h, attack 60h/63h, sustain 80h/83h, waveforms E0h/E3h, AUDIO_FM_FNUM_LOW A0h, AUDIO_FM_KEY_BLOCK B0h, AUDIO_FM_FEEDBACK C0h); the library adds the channel's offsets and bank. On an OPL3 a feedback value without an output bit (AUDIO_FM_OUT_LEFT, AUDIO_FM_OUT_RIGHT) plays on both sides. Frequencies: hz = fnum x AUDIO_FM_CHIP_HZ (49,716) x 2^block / 2^20. audioFmFree keys the voice off and gives it back; closing the context gives back all of its voices. audioFmGetInfo tells the chip, its voices and how many are free.

    -

    audioFmRawAcquire(ctx) gives one program the whole chip: every voice the others hold is keyed off and taken back (their tones go on in the mixer), new voices are refused, and audioFmRawWrite(ctx, reg, value) sets any register (bank x AUDIO_FM_BANK_REGS + index). A second program asking gets AUDIO_ERR_BUSY. audioFmRawRelease (or closing the context) silences the chip, sets it up again and gives its voices back to everyone.

    -

    audioFmAlloc, audioFmFree, audioFmWrite and audioFmRawWrite may also be called from a timer callback (platformTimerAdd); music.lib plays MIDI that way.

    +

    An OPL2 (9 voices) or OPL3 (18 voices, each playing left, right, or both) found by the opl driver is shared by every program: audioFmAlloc(ctx) takes a two-operator voice (the least recently given back first, so a released note's tail is cut last) and returns its id, AUDIO_ERR_NO_VOICE when all are taken, AUDIO_ERR_BUSY while a program has the whole chip, AUDIO_ERR_NO_SYNTH without a chip. audioFmWrite(ctx, voice, reg, value) sets a register of the voice's channel, numbered as for the first channel of an OPL2 (AUDIO_FM_MOD_CHAR 20h, AUDIO_FM_CAR_CHAR 23h, levels 40h/43h, attack 60h/63h, sustain 80h/83h, waveforms E0h/E3h, AUDIO_FM_FNUM_LOW A0h, AUDIO_FM_KEY_BLOCK B0h, AUDIO_FM_FEEDBACK C0h); the library adds the channel's offsets and bank. On an OPL3 a feedback value without an output bit (AUDIO_FM_OUT_LEFT, AUDIO_FM_OUT_RIGHT) plays on both sides. Frequencies: hz = fnum x AUDIO_FM_CHIP_HZ (49,716) x 2^block / 2^20. audioFmFree keys the voice off and gives it back; closing the context gives back all of its voices. audioFmGetInfo tells the chip, its voices, and how many are free.

    +

    audioFmRawAcquire(ctx) gives one program the whole chip: every voice the others hold is keyed off and taken back (their tones go on in the mixer), new voices are refused, and audioFmRawWrite(ctx, reg, value) sets any register (bank x AUDIO_FM_BANK_REGS + index). A second program asking gets AUDIO_ERR_BUSY. audioFmRawRelease (or closing the context) silences the chip, sets it up again, and gives its voices back to everyone.

    +

    audioFmAlloc, audioFmFree, audioFmWrite, and audioFmRawWrite may also be called from a timer callback (platformTimerAdd); music.lib plays MIDI that way.

    MIDI Port

    With DVX.INI [audio] midi = mpu401 the mpu401 driver opens an MPU-401 (or compatible, such as a Sound Blaster 16's) in UART mode, for an external synthesizer such as a Roland MT-32 or SC-55. The port belongs to one program at a time: audioMidiAcquire(ctx) (AUDIO_ERR_BUSY when another has it, AUDIO_ERR_NO_SYNTH without a port), audioMidiPut(ctx, byte) sends a byte and returns AUDIO_ERR_BUSY when the port cannot take one yet (send it again later; it may be called from a timer callback), audioMidiRelease gives it back (closing the context does too). audioMidiGetInfo tells whether there is a port and whether it is in use. music.lib uses the port for MIDI music.

    Volume and the Voice Limit

    audioSetMasterVolume sets the volume of all sound (DVX.INI [audio] volume, in percent, sets it when the device starts); audioSetVolume sets the volume of everything one context plays. At most the voice limit (DVX.INI [audio] voices, 1 to AUDIO_VOICES_MAX, default 16) plays at once. Starting another sample or tone then stops the quietest one playing (it ends with AUDIO_END_DROPPED); when the new sound would itself be the quietest it is not started (AUDIO_ERR_NO_VOICE). Streams are never dropped.

    Files

    -

    WAV: uncompressed PCM, 8-bit unsigned or 16-bit signed, mono or stereo, 4000 to 48000 Hz; WAVE_FORMAT_EXTENSIBLE with the PCM sub-format is accepted, other chunks are skipped, and a data chunk that claims more bytes than the file holds plays what is there. VOC (Creative Voice File, header checksum checked): sound blocks with 8-bit unsigned samples, continuation, silence, extended (rate and stereo for the next sound block) and new-format blocks with 8-bit or 16-bit PCM; markers and text are skipped, and a repeated section plays once. Compressed sound (ADPCM, 24-bit, floating point) gives AUDIO_ERR_UNSUPPORTED, a damaged file AUDIO_ERR_CORRUPT, a file that is neither AUDIO_ERR_FORMAT.

    +

    WAV: uncompressed PCM, 8-bit unsigned or 16-bit signed, mono or stereo, 4000 to 48000 Hz; WAVE_FORMAT_EXTENSIBLE with the PCM sub-format is accepted, other chunks are skipped, and a data chunk that claims more bytes than the file holds plays what is there. VOC (Creative Voice File, header checksum checked): sound blocks with 8-bit unsigned samples, continuation, silence, extended (rate and stereo for the next sound block), and new-format blocks with 8-bit or 16-bit PCM; markers and text are skipped, and a repeated section plays once. Compressed sound (ADPCM, 24-bit, floating point) gives AUDIO_ERR_UNSUPPORTED, a damaged file AUDIO_ERR_CORRUPT, a file that is neither AUDIO_ERR_FORMAT.

    The Mixer

    -

    All sound is mixed in 16-bit integers at the rate the device achieved (the mixer never assumes the rate it asked for: a Sound Blaster Pro asked for 22,050 Hz plays at 22,222 Hz), and every source is resampled to it with linear interpolation. Gains combine volume, pan, context volume and master volume; the sum is clipped to 16 bits. The mixer runs from the sound card's own interrupt (the Sound Blaster, at the end of each half buffer of about 23 ms), or from the timer service for devices without one, so sound keeps playing however busy the programs are. It uses no floating point and touches only memory pinned for interrupts (sample and stream buffers are pinned when they are made).

    +

    All sound is mixed in 16-bit integers at the rate the device achieved (the mixer never assumes the rate it asked for: a Sound Blaster Pro asked for 22,050 Hz plays at 22,222 Hz), and every source is resampled to it with linear interpolation. Gains combine volume, pan, context volume, and master volume; the sum is clipped to 16 bits. The mixer runs from the sound card's own interrupt (the Sound Blaster, at the end of each half buffer of about 23 ms), or from the timer service for devices without one, so sound keeps playing however busy the programs are. It uses no floating point and touches only memory pinned for interrupts (sample and stream buffers are pinned when they are made).

    Settings

    DVX.INI section [audio]:

      Key          Meaning
       ---          -------
       driver       auto (default: find the hardware), or a driver name: sb, speaker, null
       rate         mixing rate asked for, 4000 to 48000 (default 22050)
    -  bits         0 (the device's best), 8 or 16
    -  channels     0 (the device's best), 1 or 2
    +  bits         0 (the device's best), 8, or 16
    +  channels     0 (the device's best), 1, or 2
       voices       voices mixed at once, 1 to 32 (default 16)
       volume       master volume in percent (default 100)
       speakerPcm   true: sampled sound on the PC speaker (see below); default false
    @@ -6582,7 +6937,7 @@ audioFmFree(audio, v);                          // key off, back to everyone
     

    A settings screen reads and writes the keys it offers through the library, so the key names live in one place (audio.h, Settings; prefs is a copy of DVX.INI the caller loads and saves): audioSettingDriver(prefs) (AUDIO_DRIVER_AUTO when missing), audioSettingMidi(prefs) (an index in audioMidiChoices, auto when missing or unknown), audioSettingVolume(prefs) (percent, clamped to 0..100, 100 when missing), and audioSettingsStore(prefs, driver, midi, volume) writes all three (an empty driver stores auto, a midi index out of range stores auto). audioMidiChoices[AUDIO_MIDI_CHOICE_COUNT] holds the midi choices in list order, each with its label for people and the name stored (a PrefsChoiceT, dvxPrefs.h). audioPercentToVolume(percent) turns a percent into 0..AUDIO_VOLUME_MAX for audioSetMasterVolume. The device reads the driver and volume through the same functions when it starts. The Control Panel's Sounds page uses all of these.

    Desktop Sounds

    @@ -6601,7 +6956,7 @@ audioFmFree(audio, v); // key off, back to everyoneDrivers
      Driver       Device
       ------       ------
    -  sb           Sound Blaster family by DSP version: 1.x (8-bit single-cycle), 1.5 / 2.0 (8-bit auto-init, high-speed above 23,000 Hz on 2.0), Pro and Pro 2 (8-bit stereo at 11,025 or 22,050 Hz), 16 (8- or 16-bit, mono or stereo, any rate from 5,000 to 44,100 Hz). The port, IRQ and DMA channels come from the BLASTER variable (e.g. SET BLASTER=A220 I5 D1 H5); without it an SB16 is found at 220h or 240h and asked for its IRQ and DMA. If no interrupt arrives from the card within half a second (a wrong IRQ), the next driver is tried.
    +  sb           Sound Blaster family by DSP version: 1.x (8-bit single-cycle), 1.5 / 2.0 (8-bit auto-init, high-speed above 23,000 Hz on 2.0), Pro and Pro 2 (8-bit stereo at 11,025 or 22,050 Hz), 16 (8- or 16-bit, mono or stereo, any rate from 5,000 to 44,100 Hz). The port, IRQ, and DMA channels come from the BLASTER variable (e.g. SET BLASTER=A220 I5 D1 H5); without it an SB16 is found at 220h or 240h and asked for its IRQ and DMA. If no interrupt arrives from the card within half a second (a wrong IRQ), the next driver is tried.
       speaker      The PC speaker. By default tones only: the timer chip plays the loudest square or triangle tone as a square wave, with no interrupts and no CPU cost; sampled sound and noise are not heard. With speakerPcm = true it plays everything by pulse-width modulation: the timer interrupt runs at 22,096 Hz and each interrupt sets the next pulse (about 5.7 bits). That costs a lot of CPU time: 22,096 interrupts a second plus the mixing. A 486DX2-66 spends a large share of its time on it (estimated 10 to 15 percent for the interrupts alone on plain DOS, much more under a memory manager or Windows) and can lose interrupts, which slows the sound and the DVX clock down. Meant for Pentium-class machines.
       null         No sound device: plays nothing, but voices still run their course and end, so programs behave the same.
       opl          (synthesizer) The OPL2 or OPL3 FM chip of an AdLib or Sound Blaster at 388h (oplPort), found by the AdLib timer test; the status register tells an OPL2 from an OPL3. Register writes are timed by status reads (3.3 and 23 us on an OPL2) with interrupts off around each index/data pair. Closing zeroes every register.
    @@ -6609,44 +6964,44 @@ audioFmFree(audio, v);                          // key off, back to everyoneaudioDriverNames(names, max) lists the short names of the sound drivers installed, the one tried first first (for a settings screen; AUDIO_DRIVERS_MAX is the most DVX's own callers ask for).

    Sound drivers are tried in this order: the one DVX.INI names, then from the highest priority down (Sound Blaster, PC speaker, null). The first that starts is used. Synthesizer drivers are opened with the sound device, beside it: of each kind (FM chip, MIDI port) the first that finds its hardware is kept, and they close with the device. The sound device's description and hasSynth in AudioInfoT tell whether there is a synthesizer.

    Writing a Driver

    -

    A driver is a driver module (see dvxDrv.h in the API reference): <name>.drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers/<name>/ folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it.

    +

    A driver is a driver module (see dvxDrv.h in the API reference): <name>.drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers/<name>/ folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX, or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it.

    A synthesizer driver exports an AudioSynthDriverT named audioSynth instead, written with AUDIO_SYNTH_DRIVER(name, priority, .kind = AUDIO_SYNTH_FM or AUDIO_SYNTH_MIDI, .open = ..., .close = ..., .write = ... or .put = ...). open detects the hardware, fills AudioSynthDrvInfoT (description; for FM the voices and whether it is stereo) and leaves it silent; close silences it. write(reg, value) sets an FM register (bank x AUDIO_FM_BANK_REGS + index) and must keep the index/data pair together (audioIrqOff / audioIrqRestore); put(byte) sends a MIDI byte, returning false when the port is not ready. Both run at interrupt time too (timer callbacks), so they follow the rules above and are pinned by the driver.

    BASIC

    src/include/basic/sound.bas (DECLARE LIBRARY "audio"): PlayWave(fileName), PlayWaveLoop(fileName), PlayTone(frequency, milliseconds, volume), PlayNoise(frequency, milliseconds, volume) return a voice number (0 on failure: SoundError$ says why); StopWave(voice), StopAllSounds, SoundPlaying(voice), SoundVolume(percent), SoundInfo$ (the device, e.g. "Sound Blaster 16 (DSP 4.05) at 220h, 22050 Hz, 16-bit stereo"). Volumes are percent. Each BASIC program plays through its own context, closed when it ends.

    -

    For a settings program (the Control Panel's Sounds page): SoundEventCount, SoundEventLabel$(eventIdx), SoundEventKey$(eventIdx) (audioDeskLabel / audioDeskKey; events numbered from 0 in DvxSoundE order); SoundEventFile$(prefs, eventIdx) and SoundSetEventFile(prefs, eventIdx, fileName) (audioDeskFile / audioDeskSetFile); SoundEventsReload (audioDeskReload); SoundTestFile(fileName) (audioDeskPlayFile: a voice number, or 0 with SoundError$); SoundFolder$ (AUDIO_SOUNDS_DIR); SoundMasterVolume and SoundSetMasterVolume(percent) (audioGetMasterVolume / audioSetMasterVolume in percent); SoundDriverCount (reads audioDriverNames, at most AUDIO_DRIVERS_MAX) and SoundDriverName$(index); SoundDriverAuto$ (AUDIO_DRIVER_AUTO); SoundMidiCount and SoundMidiLabel$(index) (audioMidiChoices); SoundSettingDriver$(prefs), SoundSettingMidi(prefs), SoundSettingVolume(prefs) and SoundSettingsStore(prefs, driver, midi, volume) (the audioSetting functions). prefs is a prefs.bas handle (prefsBasHandle, dvxPrefs.h); a handle that is not open reads the defaults and stores nothing.

    -

    The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running" True / False; "voices" = voiceLimit, "rate", "bits", "channels"; any case, 0 for another field); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order). Samples: SoundLoad(fileName) (audioSampleLoad) and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped) and SoundSetFrequency(voice, hz) (the audioVoiceSet functions; the result is kept for SoundError$). Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamClose. FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS). The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease. CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*) and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*).

    +

    For a settings program (the Control Panel's Sounds page): SoundEventCount, SoundEventLabel$(eventIdx), SoundEventKey$(eventIdx) (audioDeskLabel / audioDeskKey; events numbered from 0 in DvxSoundE order); SoundEventFile$(prefs, eventIdx) and SoundSetEventFile(prefs, eventIdx, fileName) (audioDeskFile / audioDeskSetFile); SoundEventsReload (audioDeskReload); SoundTestFile(fileName) (audioDeskPlayFile: a voice number, or 0 with SoundError$); SoundFolder$ (AUDIO_SOUNDS_DIR); SoundMasterVolume and SoundSetMasterVolume(percent) (audioGetMasterVolume / audioSetMasterVolume in percent); SoundDriverCount (reads audioDriverNames, at most AUDIO_DRIVERS_MAX) and SoundDriverName$(index); SoundDriverAuto$ (AUDIO_DRIVER_AUTO); SoundMidiCount and SoundMidiLabel$(index) (audioMidiChoices); SoundSettingDriver$(prefs), SoundSettingMidi(prefs), SoundSettingVolume(prefs), and SoundSettingsStore(prefs, driver, midi, volume) (the audioSetting functions). prefs is a prefs.bas handle (prefsBasHandle, dvxPrefs.h); a handle that is not open reads the defaults and stores nothing.

    +

    The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running" True / False; "voices" = voiceLimit, "rate", "bits", "channels"; any case, 0 for another field); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order). Samples: SoundLoad(fileName) (audioSampleLoad) and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped), and SoundSetFrequency(voice, hz) (the audioVoiceSet functions; the result is kept for SoundError$). Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamClose. FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS). The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease. CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*), and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*).

    Sound and stream numbers are 1..64 per kind, valid only for the program that got them; audioClose of a BASIC context forgets its numbers (audioBasForget) as it frees the samples and streams. BINARY STRING parameters arrive as a pointer and a byte count (two C parameters; see the DECLARE LIBRARY rules in the BASIC reference).

    BASIC Development Library

    -

    basdev.lib is the DVX BASIC compiler and what a development tool builds on it: the build pipeline (basBuild.h), the compile-time Control.Member validator (basValidate.h), the debugger's view of a paused program (basDebug.h) and the SUB / FUNCTION scanner (basProcs.h). The DVX BASIC IDE uses it, and BASIC programs reach it through compiler.bas and debug.bas (basDevBas.c holds their entry points). It also carries the STUB and noicon resources every build inside DVX uses (BAS_BUILD_STUB_CARRIER). It needs basrt.lib, which holds the run host (basRunHost.h) the IDE, the standalone stub and debug.bas share.

    +

    basdev.lib is the DVX BASIC compiler and what a development tool builds on it: the build pipeline (basBuild.h), the compile-time Control.Member validator (basValidate.h), the debugger's view of a paused program (basDebug.h), and the SUB / FUNCTION scanner (basProcs.h). The DVX BASIC IDE uses it, and BASIC programs reach it through compiler.bas and debug.bas (basDevBas.c holds their entry points). It also carries the STUB and noicon resources every build inside DVX uses (BAS_BUILD_STUB_CARRIER). It needs basrt.lib, which holds the run host (basRunHost.h) the IDE, the standalone stub, and debug.bas share.

    Headers: apps/dvxbasic/basBuild.h, apps/dvxbasic/basdev/basValidate.h, apps/dvxbasic/basdev/basDebug.h, apps/dvxbasic/basdev/basProcs.h, apps/dvxbasic/basdev/basSource.h, apps/dvxbasic/basdev/basHelpMap.h, apps/dvxbasic/basdev/basProject.h, apps/dvxbasic/formrt/basRunHost.h

    Compiling (basBuild.h)

      Function                Description
       --------                -----------
       basBuildAddSource       Append one file to a BasProjectSourceT the way a project is built: a module in BEGINMODULE, a class module's code in BEGINCLASS, a UserControl's code in BEGINCLASS ... USERCONTROL (its header is kept in ctlTexts), a form's code section in BEGINFORM with its controls (the .frm text is kept in frmTexts), a lone program unwrapped. Records the file in the source map.
       basBuildConcatProject   Read every file an open .dbp names (modules, class modules, UserControls, forms) into a BasProjectSourceT through basBuildAddSource.
    -  basBuildCompile         Compile source into a BasModuleT with OPTION EXPLICIT and an optional BasCtrlValidatorT. A BasCompileErrorT says why it failed: BAS_COMPILE_ERROR (line, column, message), BAS_COMPILE_NO_MEMORY or BAS_COMPILE_NO_MODULE.
    +  basBuildCompile         Compile source into a BasModuleT with OPTION EXPLICIT and an optional BasCtrlValidatorT. A BasCompileErrorT says why it failed: BAS_COMPILE_ERROR (line, column, message), BAS_COMPILE_NO_MEMORY, or BAS_COMPILE_NO_MODULE.
       basBuildErrorText       A compiler message without its "Line N, Col M: " prefix.
       basBuildMapLine         Line of the concatenated source to file and file line.
       basBuildMapFileLine     File and file line to the line of the concatenated source (names compared as DOS does: any case, / and \ alike).
       basBuildSpecFromProject Fill a BasBuildSpecT's project metadata from an open .dbp.
    -  basBuildApp             Build an .app from a module, the .frm texts and the .ctl headers (ctlSources). A NULL selfPath takes the stub from BAS_BUILD_STUB_CARRIER (this library).
    -  basBuildControl         Build a control file (.wgt, resources only) from a control project's module and .ctl headers: the module, each header as CTLn with its toolbox icon24 and name resources, the project's metadata and help file. The IDE's Make Control and CompBuildApp of a control project.
    +  basBuildApp             Build an .app from a module, the .frm texts, and the .ctl headers (ctlSources). A NULL selfPath takes the stub from BAS_BUILD_STUB_CARRIER (this library).
    +  basBuildControl         Build a control file (.wgt, resources only) from a control project's module and .ctl headers: the module, each header as CTLn with its toolbox icon24 and name resources, the project's metadata, and help file. The IDE's Make Control and CompBuildApp of a control project.
       basBuildIsControlProject  True when an open .dbp is a control project ([Project] Type = Control).
       basBuildWrapForPath     The kind of a project file by its extension.

    Validating Control Members (basValidate.h)

    -

    A BasValidatorT (zeroed) collects the forms, controls and menu items of the .frm texts basValidateScanFrm parses; its onCode hook also receives each form's code section, so one parse serves both. basValidateCallbacks fills the BasCtrlValidatorT the compiler takes; pass &validator.compiler as basBuildCompile's liveCompiler so a control name resolves on the form whose code is compiled. Member names are checked against the loaded widget interfaces (WgtIfaceT) and the form runtime's own property tables. badTypes lists controls whose type no loaded widget library registers (BAS_VALIDATE_BAD_TYPE_FMT formats one). The command-line compiler does not validate: no widget library is loaded there.

    +

    A BasValidatorT (zeroed) collects the forms, controls, and menu items of the .frm texts basValidateScanFrm parses; its onCode hook also receives each form's code section, so one parse serves both. basValidateCallbacks fills the BasCtrlValidatorT the compiler takes; pass &validator.compiler as basBuildCompile's liveCompiler so a control name resolves on the form whose code is compiled. Member names are checked against the loaded widget interfaces (WgtIfaceT) and the form runtime's own property tables. badTypes lists controls whose type no loaded widget library registers (BAS_VALIDATE_BAD_TYPE_FMT formats one). The command-line compiler does not validate: no widget library is loaded there.

    Debugging (basDebug.h)

    Every function takes the paused program's VM (or NULL when there is none).

      Function                Description
       --------                -----------
       basDbgCurProcIdx        The compiled procedure the paused VM is in (-1: module level).
       basDbgProcNameForPc     Procedure name holding a code address (BAS_DBG_MODULE_FRAME before every procedure).
    -  basDbgVarVisible        Whether a debug variable is in scope where the VM is paused (QBasic rules: SHARED, STATIC, form and member variables).
    +  basDbgVarVisible        Whether a debug variable is in scope where the VM is paused (QBasic rules: SHARED, STATIC, form, and member variables).
       basDbgFindVar           The visible debug variable of a name.
       basDbgVarSlot           The live value slot of a debug variable.
    -  basDbgResolvePath       The live slot of var, var(i, j), var.field or var(i).field (a TYPE field or a class member).
    +  basDbgResolvePath       The live slot of var, var(i, j), var.field, or var(i).field (a TYPE field or a class member).
       basDbgFormatValue       Display text of a value (strings quoted, #dates#, array bounds, class names, Collection item counts).
       basDbgLocals            The visible variables as rows (name, type, value) through a BasDbgRowFnT.
       basDbgCallStack         The call stack as rows (procedure, line).
    @@ -6657,20 +7012,20 @@ audioFmFree(audio, v);                          // key off, back to everyone
     

    Procedures (basProcs.h)

    -

    basProcDeclAt, basProcSkipToEnd and basProcNameFromDecl find the SUB / FUNCTION blocks of source text without compiling it (BAS_PROC_KW_END_SUB / BAS_PROC_KW_END_FUNCTION end them). The IDE's one-procedure-at-a-time editor and CompProcList$ use them.

    +

    basProcDeclAt, basProcSkipToEnd, and basProcNameFromDecl find the SUB / FUNCTION blocks of source text without compiling it (BAS_PROC_KW_END_SUB / BAS_PROC_KW_END_FUNCTION end them). The IDE's one-procedure-at-a-time editor and CompProcList$ use them.

    Source Text (basSource.h)

    -

    Source text read with the compiler's lexer, line by line, on text that need not compile: basSourceTokens reports every token with its kind (BasSrcKindE: keyword, identifier, string, number, date, symbol, comment, error), line, column and source span; basSourceInCode tells whether an offset is code (not in a string literal or a comment); basSourceRename renames a form or control (oldName followed by '.' or '_' as a whole word), optionally only in code; basSourceWithEvents and basSourceClassEvents report the WithEvents variables and the Events of a class module. The IDE's rename (ideRenameInCode, also after the Menu Editor renames items) and its Object / Event lists use them; compiler.bas has CompTokens$, CompInCode, CompRenameInCode$, CompWithEvents$ and CompClassEvents$.

    +

    Source text read with the compiler's lexer, line by line, on text that need not compile: basSourceTokens reports every token with its kind (BasSrcKindE: keyword, identifier, string, number, date, symbol, comment, error), line, column, and source span; basSourceInCode tells whether an offset is code (not in a string literal or a comment); basSourceRename renames a form or control (oldName followed by '.' or '_' as a whole word), optionally only in code; basSourceWithEvents and basSourceClassEvents report the WithEvents variables and the Events of a class module. The IDE's rename (ideRenameInCode, also after the Menu Editor renames items) and its Object / Event lists use them; compiler.bas has CompTokens$, CompInCode, CompRenameInCode$, CompWithEvents$, and CompClassEvents$.

    Help Topics (basHelpMap.h)

    basHelpKeywordTopic gives the DVX BASIC help topic of a keyword or built-in function (NULL for none) and basHelpCtrlTopic the topic of a control type (BAS_HELP_CTRL_PREFIX and the type name in lower case): what F1 opens in the IDE, and CompHelpTopic$ / CompHelpCtrlTopic$.

    Projects (basProject.h)

    -

    The project (.dbp) model: PrjStateT with its file list (PrjFileT: path relative to the project's folder, kind -- PrjFileKindE: module, class module, UserControl ([Controls], .ctl) or form, in compile order -- edit buffer) and settings, prjLoad / prjSave (one table of [Project] / [Settings] keys drives load, save, prjFieldGet and prjFieldSet; [Project] Type = Control makes a control project, prjIsControlProject), prjNew, prjAddFile, prjRemoveFile, prjRenameFile (disk and project together), prjFileIndexByPath, prjKindForPath (the build's rule, basBuildWrapForPath), prjKindWrap, prjClassName (a class module's or UserControl's class name), prjReadFileText and the source map of the last compile (prjMapLine, prjMapFileLine). The IDE keeps only its Project window and Properties dialog (ideProject.c); compiler.bas has ProjOpen, ProjNew, ProjSave, ProjClose, ProjPath$, ProjFileCount, ProjFile$, ProjFilePath$, ProjFileKind, ProjFileFind, ProjAddFile, ProjRemoveFile, ProjRenameFile, ProjGet$ and ProjSet; CompKeywordClass and CompKeywordReserved expose the keyword table's flags.

    -

    A UserControl's header (the Begin UserControl block of a .ctl) is read and changed key by key, every other line of the text kept: prjCtlKeyAt / prjCtlKeyFind give the keys (PrjCtlKeyT: name and PrjCtlValueE kind -- Name, the control's name on the Begin line, then Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, DefaultEvent), prjCtlHeaderGet the value the control takes (its default when the header leaves the key out), prjCtlHeaderSet a malloc'd copy of the text with one key changed, added before End or removed (an empty value), written as its kind is (a text in quotes, a color &HBBGGRR&, True or False), and prjCtlNew the text of a new UserControl (its header with Width and Height, Option Explicit, an empty UserControl_Paint written with PRJ_HANDLER_FMT and PRJ_PRIVATE, the form the IDE writes event handlers in). The IDE's File > New UserControl and Properties window use them; compiler.bas has ProjCtlNew$, ProjCtlHeader$, ProjCtlSetHeader$ and ProjCtlKeys$ (the keys and their kinds, prjCtlKeyAt order).

    +

    The project (.dbp) model: PrjStateT with its file list (PrjFileT: path relative to the project's folder, kind -- PrjFileKindE: module, class module, UserControl ([Controls], .ctl), or form, in compile order -- edit buffer) and settings, prjLoad / prjSave (one table of [Project] / [Settings] keys drives load, save, prjFieldGet, and prjFieldSet; [Project] Type = Control makes a control project, prjIsControlProject), prjNew, prjAddFile, prjRemoveFile, prjRenameFile (disk and project together), prjFileIndexByPath, prjKindForPath (the build's rule, basBuildWrapForPath), prjKindWrap, prjClassName (a class module's or UserControl's class name), prjReadFileText, the fonts the program carries ([Fonts]: prjAddFont, prjRemoveFont, prjFontCount, prjFontPath; the build makes them FONTn font resources, basBuildSpecT fonts), and the source map of the last compile (prjMapLine, prjMapFileLine). The IDE keeps only its Project window and Properties dialog (ideProject.c); compiler.bas has ProjOpen, ProjNew, ProjSave, ProjClose, ProjPath$, ProjFileCount, ProjFile$, ProjFilePath$, ProjFileKind, ProjFileFind, ProjAddFile, ProjRemoveFile, ProjRenameFile, ProjFontCount, ProjFont$, ProjAddFont, ProjRemoveFont, ProjGet$, and ProjSet; CompKeywordClass and CompKeywordReserved expose the keyword table's flags.

    +

    A UserControl's header (the Begin UserControl block of a .ctl) is read and changed key by key, every other line of the text kept: prjCtlKeyAt / prjCtlKeyFind give the keys (PrjCtlKeyT: name and PrjCtlValueE kind -- Name, the control's name on the Begin line, then Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, DefaultEvent), prjCtlHeaderGet the value the control takes (its default when the header leaves the key out), prjCtlHeaderSet a malloc'd copy of the text with one key changed, added before End, or removed (an empty value), written as its kind is (a text in quotes, a color &HBBGGRR&, True or False), and prjCtlNew the text of a new UserControl (its header with Width and Height, Option Explicit, an empty UserControl_Paint written with PRJ_HANDLER_FMT and PRJ_PRIVATE, the form the IDE writes event handlers in). The IDE's File > New UserControl and Properties window use them; compiler.bas has ProjCtlNew$, ProjCtlHeader$, ProjCtlSetHeader$, and ProjCtlKeys$ (the keys and their kinds, prjCtlKeyAt order).

    Running a Program (basRunHost.h, in basrt.lib)

    -

    basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path and its CONFIG / DATA folders, COMMAND$, the print, input and DoEvents callbacks, the screen window's name, the help file and whether runtime errors show their own box. basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs.

    +

    basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path and its CONFIG / DATA folders, COMMAND$, the print, input, and DoEvents callbacks, the screen window's name, the help file, and whether runtime errors show their own box. basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused, and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs.

    VM accessors a debugger host uses (vm.h): basVmSetAppPaths, basVmSetBreakpointCallback, basVmSetDebugPaused, basVmResume, basVmStop, basVmClearStepState, basVmGetCurrentLine, basVmGetErrorLine.

    From BASIC

    -

    compiler.bas and debug.bas declare the entry points (see Include Libraries in the BASIC language reference). Source sets, modules and runs are handles freed with their program. A run is polled: RunPoll runs the child for a bounded time and never waits. A pause in the child's module code returns RUN_PAUSED; a pause inside one of its event handlers (or its forms' Load code, inside RunStart) waits in place while the child pumps DVX events, so the caller's Timer and menus can step, continue or stop it; RunPoll called from there returns at once. RunFree while the child's code is on the stack stops it at once and releases it at the next library call after that code has unwound.

    -

    CompDesignControls registers a source set's UserControls for the calling program's design forms (basCtlLoadDesign with its runtime, the current one: a child run's own when the child calls it) and holds the module -- one of the caller's own handles; another program's is refused -- until CompDesignRelease, the next CompDesignControls or the end of that runtime (appEnded for a program, runDestroyHost for a child run), CompModuleFree meanwhile only marking it.

    +

    compiler.bas and debug.bas declare the entry points (see Include Libraries in the BASIC language reference). Source sets, modules, and runs are handles freed with their program. A run is polled: RunPoll runs the child for a bounded time and never waits. A pause in the child's module code returns RUN_PAUSED; a pause inside one of its event handlers (or its forms' Load code, inside RunStart) waits in place while the child pumps DVX events, so the caller's Timer and menus can step, continue, or stop it; RunPoll called from there returns at once. RunFree while the child's code is on the stack stops it at once and releases it at the next library call after that code has unwound.

    +

    CompDesignControls registers a source set's UserControls for the calling program's design forms (basCtlLoadDesign with its runtime, the current one: a child run's own when the child calls it) and holds the module -- one of the caller's own handles; another program's is refused -- until CompDesignRelease, the next CompDesignControls, or the end of that runtime (appEnded for a program, runDestroyHost for a child run), CompModuleFree meanwhile only marking it.

    BASIC Runtime Library

    @@ -6696,8 +7051,8 @@ audioFmFree(audio, v); // key off, back to everyone -

    A reference only ever lives in a call's parameters (a ByRef parameter, a ParamArray element). Read through it with basValDeref, which returns the value a reference names (or the value itself). refKind says what ref.owner is: BAS_REF_SLOT (none: a global, local or form variable), BAS_REF_ARRAY (the array, counted and locked so REDIM / ERASE raise error 10), BAS_REF_UDT (the TYPE instance holding the field), BAS_REF_OBJECT (the class instance holding the member variable) or BAS_REF_CELL (a BasRefCellT holding both the locked array and the TYPE instance, for a field of an array element). refType is the target's declared type (stores convert to it) and refLen the length of a STRING * n target. basValCopy and basValRelease count the owner.

    + BAS_TYPE_REF 9 ref Reference (ByRef argument): ref.target is the variable, element, or field it names; ref.owner keeps that storage alive. +

    A reference only ever lives in a call's parameters (a ByRef parameter, a ParamArray element). Read through it with basValDeref, which returns the value a reference names (or the value itself). refKind says what ref.owner is: BAS_REF_SLOT (none: a global, local, or form variable), BAS_REF_ARRAY (the array, counted and locked so REDIM / ERASE raise error 10), BAS_REF_UDT (the TYPE instance holding the field), BAS_REF_OBJECT (the class instance holding the member variable), or BAS_REF_CELL (a BasRefCellT holding both the locked array and the TYPE instance, for a field of an array element). refType is the target's declared type (stores convert to it) and refLen the length of a STRING * n target. basValCopy and basValRelease count the owner.

    BasValueT

    Tagged union holding any BASIC value.

    struct BasValueTag {
    @@ -6956,9 +7311,9 @@ int32_t     basVmCollectionAdd(BasObjectT *coll, BasValueT item, const char *key
     
      Function                                                  Description
       --------                                                  -----------
       basCtlIface(typeName)                                     The widget interface of a BASIC type name, or NULL.
    -  basCtlPropAt(typeName, idx, out) / basCtlPropFind(...)    A type's properties as BasCtlPropT (name, type, readable, writable, enum names): the runtime table's ("Form", "Menu" or the common one), the widget's own winning on a name, then the widget's others. The IDE's properties window uses basCtlPropFind.
    +  basCtlPropAt(typeName, idx, out) / basCtlPropFind(...)    A type's properties as BasCtlPropT (name, type, readable, writable, enum names): the runtime table's ("Form", "Menu", or the common one), the widget's own winning on a name, then the widget's others. The IDE's properties window uses basCtlPropFind.
       basCtlMethodAt / basCtlEventAt(typeName, idx)             A type's methods (the widget's, then the common ones) and events (the master BAS_EVENT_LIST's for that kind of object, then the widget's); the IDE's event list uses basCtlEventAt.
    -  basCtlEventParams(typeName, eventName)                    The parameter list of a handler of a type's event: the widget's own (WgtEventDescT params), else the master list's; typeName NULL searches every widget after the master list, BAS_UC_OBJECT ("UserControl", the object in a UserControl's own code) has BAS_UC_EVENT_LIST's alone. The IDE's handler skeletons, CtlEventParams and CtlTypeEventParams.
    +  basCtlEventParams(typeName, eventName)                    The parameter list of a handler of a type's event: the widget's own (WgtEventDescT params), else the master list's; typeName NULL searches every widget after the master list, BAS_UC_OBJECT ("UserControl", the object in a UserControl's own code) has BAS_UC_EVENT_LIST's alone. The IDE's handler skeletons, CtlEventParams, and CtlTypeEventParams.
       basCtlDefaultEvent, basCtlNamePrefix, basCtlIsContainer   As the toolbox and designer need them.
       basCtlResPath(typeName, resBase, resName, size)           The widget file and the (suffixed) resource name of its toolbox icon (BAS_CTL_RES_ICON) or tooltip (BAS_CTL_RES_NAME).
       basCtlTypeAt(rt, idx)                                     The idx-th control type rt sees (basUcIfaceVisible; rt NULL: a C host) in alphabetical order of BASIC name, NULL past the end. The IDE's toolbox and CtlTypeName list the types in this order; the registry's own (wgtIfaceAt) is the order the widget files loaded in.
    @@ -6966,9 +7321,9 @@ int32_t     basVmCollectionAdd(BasObjectT *coll, BasValueT item, const char *key
       basDsgnPaintHandles(ctx, win, w)                          Paints a widget's eight selection handles over a window's painted content.
       basDsgnPropertyChanged(w, propName)                       The code of UserControl widget w called PropertyChanged: a form in DesignMode holding it raises DesignPropertyChanged (CtrlName, PropertyName). formCtl.c calls it beside the C hook (basUcSetPropChangedHook).
       basDsgnWidgetHit(w, x, y)                                 True when a point is on a visible widget.
    -

    Form.DesignMode replaces the form window's mouse, key, paint and cursor handlers while it is True (the controls get no input, and fireCtrlEvent drops their events), raises DesignMouse, DesignKey and DesignPropertyChanged, and paints DesignSelection's handles after the widgets. Form.Controls, Control.Parent / Container and MoveBefore (basFormRtMoveControl) are dispatched there too.

    +

    Form.DesignMode replaces the form window's mouse, key, paint, and cursor handlers while it is True (the controls get no input, and fireCtrlEvent drops their events), raises DesignMouse, DesignKey, and DesignPropertyChanged, and paints DesignSelection's handles after the widgets. Form.Controls, Control.Parent / Container, and MoveBefore (basFormRtMoveControl) are dispatched there too.

    UserControls for a Host

    -

    formrt/formCtl.c registers UserControls (widget classes written in BASIC) with the widget system. A program's own controls (basFormRtRegisterCtl, the stub and the IDE's runs) and a designer's (basCtlLoadDesign) get private registry keys, the class name plus a serial, so the same project running and being designed, or two programs, never share one; a control file's (basCtlLoadFile) keeps its name. Resolve names through these instead of wgtGetIface:

    +

    formrt/formCtl.c registers UserControls (widget classes written in BASIC) with the widget system. A program's own controls (basFormRtRegisterCtl: the stub's and the IDE's runs) and a designer's (basCtlLoadDesign) get private registry keys, the class name plus a serial, so the same project running and being designed, or two programs, never share one; a control file's (basCtlLoadFile) keeps its name. Resolve names through these instead of wgtGetIface:

      Function                                                  Description
       --------                                                  -----------
       basCtlLoadDesign(rt, module, headers, count, path)        Designer rt's controls (rt: a BASIC program designing forms, compiler.bas CompDesignControls; NULL: a C host, the IDE): the .ctl headers' classes of module (the caller keeps it until basCtlUnloadDesign or rt's end), run without the module-level code, App.Path the folder of path. Returns how many were registered (a name a native or installed control has, or rt's own control has, is not). The set rt registered before is let go first.
    @@ -7034,7 +7389,7 @@ void    basVmSetAppPaths(BasVmT *vm, const char *path,
       basVmStop                   Stop a debugged program: step state cleared, breakpoints detached, break mode ended, the running code unwinds.
       basVmGetCurrentLine         Get the current source line number (from the last OP_LINE instruction).
       basVmGetErrorLine           The source line where the current runtime error was raised (0: none).
    -  basVmSetAppPaths            Set App.Path, App.Config and App.Data.
    + basVmSetAppPaths Set App.Path, App.Config, and App.Data.

    The breakpoint callback notifies the host when a breakpoint fires during nested sub calls:

    typedef void (*BasBreakpointFnT)(void *ctx, int32_t line);

    BasModuleT

    @@ -7102,33 +7457,33 @@ void basVmSetAppPaths(BasVmT *vm, const char *path,

    The .frm Model

    -

    A form file as data, with no widgets and no windows: the form's properties, its controls (type, name, control array index, parent, the built-in size and state fields and every other "Key = Value" pair as text), its menu items and its code section. frmModelLoad reads .frm text (through frmParse) and frmModelWrite writes the canonical text the DVX BASIC IDE saves. The IDE's designer, properties window, menu editor, copy and paste all work on this model and keep only their live widgets on the side (each control's userData); designer.bas binds it for BASIC programs (the Frm* functions, formrt/frmModelBas.c).

    +

    A form file as data, with no widgets and no windows: the form's properties, its controls (type, name, control array index, parent, the built-in size and state fields, and every other "Key = Value" pair as text), its menu items, and its code section. frmModelLoad reads .frm text (through frmParse) and frmModelWrite writes the canonical text the DVX BASIC IDE saves. The IDE's designer, properties window, menu editor, and copy and paste all work on this model and keep only their live widgets on the side (each control's userData); designer.bas binds it for BASIC programs (the Frm* functions, formrt/frmModelBas.c).

    Header: apps/dvxbasic/formrt/frmModel.h (part of basrt.lib)

    Types

      Type            Description
       ----            -----------
    -  FrmModelT       A form: name, caption, width / height / left / top, padding, spacing, windowState, layout, helpTopic, contextMenu, centered / autoSize / resizable, controls (stb_ds array of FrmCtrlT pointers in tree order: each control directly followed by its children), menuItems (stb_ds array), props (other runtime form properties that were set: Icon, Tag, MousePointer ...), code (malloc'd, NULL = none), dirty (changed by an edit since it was made or loaded).
    -  FrmCtrlT        A control: name, typeName, parentName + parentIndex (the parent's control array index, so a child of one member of a container array belongs to that member), index (FRM_NO_INDEX: not in an array), left, top, width (MinWidth), height (MinHeight), maxWidth, maxHeight, weight, dragMode, helpTopic, visible, enabled, props (stb_ds array of FrmPropT: every other key, unquoted), userData (the caller's; the model never reads it and loads, copies and pastes start it at NULL).
    +  FrmModelT       A form: name, caption, width / height / left / top, padding, spacing, windowState, layout, helpTopic, contextMenu, centered / autoSize / resizable, controls (stb_ds array of FrmCtrlT pointers in tree order: each control directly followed by its children), menuItems (stb_ds array), props (other runtime form properties that were set: Icon, Tag, MousePointer, the Font properties ...), code (malloc'd, NULL = none), dirty (changed by an edit since it was made or loaded).
    +  FrmCtrlT        A control: name, typeName, parentName + parentIndex (the parent's control array index, so a child of one member of a container array belongs to that member), index (FRM_NO_INDEX: not in an array), left, top, width (MinWidth), height (MinHeight), maxWidth, maxHeight, weight, dragMode, helpTopic, visible, enabled, props (stb_ds array of FrmPropT: every other key, unquoted), userData (the caller's; the model never reads it and loads, copies, and pastes start it at NULL).
       FrmMenuItemT    A menu item: caption ("-" for a separator), name, shortcut, shortcutText, level (0: a menu-bar menu or a popup; 1: its items; 2+: submenus), id (an editor identity, see frmModelMenuReplace), checked, checkable (Checked was written), radioCheck, enabled, visible (False on a level-0 item: a popup menu).
       FrmPropT        One stored "Key = Value": name, value.
       FrmIntPropT     One built-in integer property (Left, Top, MinWidth alias Width, MinHeight alias Height, MaxWidth, MaxHeight, Weight, DragMode): name, alias, offset into FrmCtrlT, saveRule (FRM_SAVE_ALWAYS / IF_NONZERO / IF_POSITIVE).
    -

    Only values somebody set are stored and written: a property a control has by default (a PictureBox's BorderStyle, a TextBox's MaxLength) is not in the model and not in the file. Values are written in the canonical form for their widget interface type (STRING quoted, BOOL True / False, INT a number, FLOAT and ENUM unquoted); keys the widget does not know are written as quoted strings. The limits come from the parser's: FRM_MAX_NEST_DEPTH control levels, FRM_MAX_MENU_LEVEL menu levels, and no property line longer than FRM_MAX_LINE_LEN - 1 characters (the writer fails with FRM_WRITE_LINE_TOO_LONG rather than write one the parser would cut).

    +

    Only values somebody set are stored and written: a property a control has by default (a PictureBox's BorderStyle, a TextBox's MaxLength) is not in the model and not in the file. Values are written in the canonical form for their widget interface type, or for a key the widget does not know, for their type in the runtime's common property table (STRING quoted, BOOL True / False, INT a number, FLOAT and ENUM unquoted); other keys are written as quoted strings. The limits come from the parser's: FRM_MAX_NEST_DEPTH control levels, FRM_MAX_MENU_LEVEL menu levels, and no property line longer than FRM_MAX_LINE_LEN - 1 characters (the writer fails with FRM_WRITE_LINE_TOO_LONG rather than write one the parser would cut).

    Loading and Writing

      Function                                      Description
       --------                                      -----------
       frmModelNew(name)                             A blank form with the runtime form's defaults.
       frmModelLoad(text, len)                       The model of .frm text; NULL when it is not a form (no Begin Form, frmParse refuses it, two controls share a name and index) or out of memory. A Key = Value line is the innermost open block's, also after the blocks inside it, as the runtime reads it; a child names its container by the container's final Index. Of several OptionButtons of one container saved with Value True, the last keeps it.
    -  frmModelFree(m)                               Frees the model, its controls and menus.
    +  frmModelFree(m)                               Frees the model, its controls, and menus.
       frmModelWrite(m, buf, bufSize)                The .frm text into buf: its length, or FRM_WRITE_TOO_LARGE / FRM_WRITE_LINE_TOO_LONG (never a partial text).
       frmModelWriteSize(m)                          A buffer size that always holds the text.
       frmModelText(m, &len)                         The text in a new malloc'd string; NULL with len set to the FRM_WRITE_* code.
       frmModelSetCode(m, code)                      Replaces the code section.
       frmCodeSection(trailing, len, &outLen)        The code section of the text after a form's End (blank lines skipped, the first code line keeps its indent).
    -  frmModelFromForm(rt, form)                    A model of a loaded form as it is now: its properties, its controls in their containers with each property that differs from a new control of the type (compared with one made in a hidden window), its menus; no code. What FormText$ writes.
    + frmModelFromForm(rt, form) A model of a loaded form as it is now: its properties, its controls in their containers with each property that differs from a new control of the type (compared with one made in a hidden window; the six Font properties of the form and of each control that has a font of its own), its menus; no code. What FormText$ writes.

    Properties

      Function                                              Description
       --------                                              -----------
    -  frmModelFormProp(m, name, out, size)                  A form property as text (an unset design property gives "", "0" or "False"); false for run-time-only ones (Visible, DesignMode, DesignSelection, object properties).
    +  frmModelFormProp(m, name, out, size)                  A form property as text (an unset design property gives "", "0", or "False"); false for run-time-only ones (Visible, DesignMode, DesignSelection, object properties).
       frmModelSetFormProp(m, name, value)                   Sets one from text; Name renames the form; an empty value takes an optional one out.
       frmCtrlPropText(ctrl, name, out, size)                A control property as the control holds it (Name, Index, the built-in integers, HelpTopic, Visible / Enabled unless the widget defines them, the stored ones).
       frmCtrlSetPropText(ctrl, key, value)                  Applies one the way the loader does (a VB TabIndex is dropped unless the widget defines it).
    @@ -7136,6 +7491,15 @@ void    basVmSetAppPaths(BasVmT *vm, const char *path,
       frmCtrlGetProp / frmCtrlSetProp / frmCtrlRemoveProp   The stored properties.
       frmIntPropAt / frmIntPropFind / frmIntPropGet / Set   The built-in integer table.
       frmIfaceProp(typeName, propName)                      The widget interface property of a type, NULL when none.
    +

    Font Properties

    +

    FontName, FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru (BAS_PROP_FONT*) are in both the form table and the common control table. They are one font, a DvxFontChoiceT (dvxDlg.h: family, size in pixels, FONT_STYLE_* flags), and the widget's font (wgtFont, wgtSetFont) is the only place they live: a read gives the font the object draws with (a form's is its root widget's), a write gives it that font with the one part changed. The .frm loader collects a block's Font properties and opens one font when the block's properties are read, so their order in the file does not matter. Helpers in formrt.h, for loaders and designers that hold them as .frm text:

    +
      Function                                        Description
    +  --------                                        -----------
    +  basFontPropAt(idx)                              The Font property names in .frm order (FontName, FontSize, then the flags), NULL past the end.
    +  basFontPropIs(name)                             True for a Font property name (any case).
    +  basFontChoiceOf(font, out)                      A BitmapFontT as the properties name it (dvxFontFamilyOf, pixelSize, style).
    +  basFontPropText(choice, name, out, size)        One property of a choice as .frm text (the family, the size, True / False).
    +  basFontPropSetText(choice, name, text)          Sets one property of a choice from .frm text.

    Editing Controls

      Function                                                  Description
       --------                                                  -----------
    @@ -7144,7 +7508,7 @@ void    basVmSetAppPaths(BasVmT *vm, const char *path,
       frmModelRenameCtrl(m, oldName, newName)                   Renames a whole control array; children and DataSource / MasterSource of a renamed Data control follow. Identifier rules are the caller's.
       frmModelMakeArrayMember(m, ctrl, index)                   Gives a control array index; its children follow.
       frmModelMoveCtrl(m, ctrl, before)                         Moves ctrl with its subtree before a sibling (NULL: after the last).
    -  frmModelReparent(m, ctrl, parent)                         Makes ctrl the last child of another container (refuses cycles, non-containers and too deep a nesting).
    +  frmModelReparent(m, ctrl, parent)                         Makes ctrl the last child of another container (refuses cycles, non-containers, and too deep a nesting).
       frmModelSibling(m, idx, dir)                              The sibling next to a control (+1 after its subtree, -1 before).
       frmModelFindCtrl / CtrlPos / FindParent / CtrlDepth       Lookups.
       frmModelIsDescendant / SubtreeEnd / NormalizeOrder        The tree order.
    @@ -7187,14 +7551,14 @@ if (m) {
     

    Connection Library

    -

    One interface for byte-stream connections: a serial port, a modem with an AT command dialer, the secure link to the DVX host proxy (over a COM port or over the network), and Telnet. Every call except the host name lookup is non-blocking: connOpen starts the connection (dialing, TCP connect, key exchange) and connPoll moves it along, fills the receive buffer and sends what connWrite queued. Call connPoll as often as you can (every frame). The lookup of a host name waits for the answer, but other programs keep running meanwhile; a dotted address needs no lookup.

    -

    Loaded as: bin/libs/kpunch/conn/conn.lib (needs the serial, networking and transfer libraries; the transfer library for its BASIC functions)

    +

    One interface for byte-stream connections: a serial port, a modem with an AT command dialer, the secure link to the DVX host proxy (over a COM port or over the network), and Telnet. Every call except the host name lookup is non-blocking: connOpen starts the connection (dialing, TCP connect, key exchange) and connPoll moves it along, fills the receive buffer, and sends what connWrite queued. Call connPoll as often as you can (every frame). The lookup of a host name waits for the answer, but other programs keep running meanwhile; a dotted address needs no lookup.

    +

    Loaded as: bin/libs/kpunch/conn/conn.lib (needs the serial, networking, and transfer libraries; the transfer library for its BASIC functions)

    Header: conn/conn.h

    Connection Types

      Type         Behaviour
       ----         ---------
    -  ConnSerialE  A COM port with the given speed, data bits, parity, stop bits and flow control (none, XON/XOFF, RTS/CTS). Open at once.
    -  ConnModemE   The COM port, then the init string (waits for OK), then dial prefix + phone number. CONNECT opens the connection; BUSY, NO ANSWER and NO CARRIER dial again after redialDelay seconds, up to redialCount more times; NO DIAL TONE and ERROR end it. An empty phone number opens the port for typing AT commands. When the carrier (DCD) has been seen and drops, the connection ends. Closing drops DTR, which makes the modem hang up. Everything the modem says is also delivered as received data.
    +  ConnSerialE  A COM port with the given speed, data bits, parity, stop bits, and flow control (none, XON/XOFF, RTS/CTS). Open at once.
    +  ConnModemE   The COM port, then the init string (waits for OK), then dial prefix + phone number. CONNECT opens the connection; BUSY, NO ANSWER, and NO CARRIER dial again after redialDelay seconds, up to redialCount more times; NO DIAL TONE and ERROR end it. An empty phone number opens the port for typing AT commands. When the carrier (DCD) has been seen and drops, the connection ends. Closing drops DTR, which makes the modem hang up. Everything the modem says is also delivered as received data.
       ConnSecureE  The secure link (secLink): the key exchange runs in the background, authenticated when a passphrase is set (both sides must use the same one, or none). Only encrypted, authenticated data on channel 0 is delivered. With host empty it runs on the COM port; with host set it runs over a TCP connection to host:tcpPort (the proxy's listening port, 2323 by default), through secLinkOpenStream.
       ConnTelnetE  TCP to host:tcpPort with Telnet (RFC 854). Negotiated: the host may ECHO and SUPPRESS-GO-AHEAD; BINARY both ways; TERMINAL-TYPE answered "ANSI"; NAWS sends the size given to connSetWindowSize (80 x 25 until then, sent again on every change). Other options are refused, and an option is only answered when its state changes. 0xFF is doubled on the way out and undoubled on the way in; outside binary mode a lone CR goes out as CR NUL and CR NUL comes in as CR.

    The Telnet protocol itself is a separate filter (connTelnet.c) that only moves bytes, so it is tested byte for byte without a network.

    @@ -7203,7 +7567,7 @@ if (m) { ----- ------- type ConnTypeE. comPort RS232_COM1 .. RS232_COM4. - bps, dataBits, parity Line settings (parity 'N', 'E', 'O', 'M' or 'S'). + bps, dataBits, parity Line settings (parity 'N', 'E', 'O', 'M', or 'S'). stopBits, flow 1 or 2 stop bits; RS232_HANDSHAKE_* flow control. initString, dialPrefix Modem: sent before dialing (empty = none); dial command such as ATDT. phone Modem: the number (empty = no dialing). @@ -7216,14 +7580,14 @@ if (m) { ConnT *connCreate(void) Allocate a closed connection. void connDestroy(c) Close and free. void connConfigDefaults(cfg) COM1, 9600 8N1, no flow control, ATZ, ATDT. - int32_t connOpen(c, cfg, nowMs) Start connecting (cfg is copied). CONN_OK, CONN_ERR_PARAM, CONN_ERR_PORT or CONN_ERR_HOST (the host was not found or refused at once); connStatus explains. Later failures (refused, no answer within 30 seconds, connection lost, host closed it) end the connection with the reason in connStatus. + int32_t connOpen(c, cfg, nowMs) Start connecting (cfg is copied). CONN_OK, CONN_ERR_PARAM, CONN_ERR_PORT, or CONN_ERR_HOST (the host was not found or refused at once); connStatus explains. Later failures (refused, no answer within 30 seconds, connection lost, host closed it) end the connection with the reason in connStatus. void connPoll(c, nowMs) Dial, key exchange, receive, send, timers. int32_t connRead(c, buf, max) Take received bytes (0 when none). int32_t connWrite(c, data, len) Queue bytes to send; returns the count accepted (0 while not open or when the 8 KB send buffer is full). int32_t connPending(c) Bytes queued (here and in the layer below) not yet sent. bool connSendBreak(c, nowMs) Hold a break for 300 ms (serial and modem). void connClose(c) Close; a modem hangs up. - ConnStateE connState(c) ConnClosedE, ConnOpeningE (dialing, key exchange) or ConnOpenE. + ConnStateE connState(c) ConnClosedE, ConnOpeningE (dialing, key exchange), or ConnOpenE. const char *connStatus(c) What is happening, or why the connection ended, in plain words. uint32_t connStatusSerial(c) Changes whenever the status text changes. void connSetWindowSize(c, cols, rows) The terminal size for Telnet's NAWS (sent now if the host asked for it). @@ -7242,8 +7606,8 @@ connOpen(c, &cfg, nowMs()); // every frame: connPoll(c, nowMs()); n = connRead(c, buf, sizeof(buf));
    -

    A Telnet session is the same with cfg.type = ConnTelnetE, cfg.host = "bbs.example.org" and cfg.tcpPort = 23.

    -

    A file transfer runs over a connection by giving the transfer library connRead, connWrite and connPending as its XferIoT callbacks (ZMODEM over Telnet works: 0xFF bytes are escaped by the Telnet filter).

    +

    A Telnet session is the same with cfg.type = ConnTelnetE, cfg.host = "bbs.example.org", and cfg.tcpPort = 23.

    +

    A file transfer runs over a connection by giving the transfer library connRead, connWrite, and connPending as its XferIoT callbacks (ZMODEM over Telnet works: 0xFF bytes are escaped by the Telnet filter).

    Connections from BASIC

    @@ -7270,16 +7634,16 @@ n = connRead(c, buf, sizeof(buf)); ConnZmodemRequest(h) True once after a ZMODEM sender started (xferDetectFeed). ConnSendFiles(h, proto, files$, escapeCtl, resume) Send files (one path per line). An xfer.bas handle, with XFER_FAILED and the reason when it could not start; 0 when the line is not open or busy. ConnReceiveFiles(h, proto, target$, exists, escapeCtl) Receive (exists: XFER_EXIST_). -

    ConnPoll with a Terminal attached does what the C Terminal's heartbeat did: it reads up to 4 KB, appends them to the capture file, answers each ENQ with the answerback (ENQ itself is not shown), stops at a ZMODEM start frame (the rest of what was read is dropped, and ConnZmodemRequest becomes True), and writes the rest to the Terminal. While a transfer started on the connection runs (or waits for XferAnswer), ConnPoll moves it along instead, nothing reaches the Terminal and typing is dropped; a transfer on a line that is no longer open is cancelled. The receive window of a ZMODEM download on a serial or modem line without flow control is 2 KB, so a slow frame never loses data, unless the program set one with ConnSetXferWindow.

    -

    The Terminal binding goes through the form runtime (basFormRtTermBind, basFormRtTermUnbind and basFormRtTermForget in basrt, looked up when ConnAttach first needs them): the runtime unbinds a Terminal whose form unloads, and tells the library, so a connection never writes to a freed control. Every handle belongs to the program that made it; whatever it leaves open is closed (a modem hangs up, a capture file is closed, a transfer cancelled) when it ends or is ended.

    +

    ConnPoll with a Terminal attached does what the C Terminal's heartbeat did: it reads up to 4 KB, appends them to the capture file, answers each ENQ with the answerback (ENQ itself is not shown), stops at a ZMODEM start frame (the rest of what was read is dropped, and ConnZmodemRequest becomes True), and writes the rest to the Terminal. While a transfer started on the connection runs (or waits for XferAnswer), ConnPoll moves it along instead, nothing reaches the Terminal, and typing is dropped; a transfer on a line that is no longer open is cancelled. The receive window of a ZMODEM download on a serial or modem line without flow control is 2 KB, so a slow frame never loses data, unless the program set one with ConnSetXferWindow.

    +

    The Terminal binding goes through the form runtime (basFormRtTermBind, basFormRtTermUnbind, and basFormRtTermForget in basrt, looked up when ConnAttach first needs them): the runtime unbinds a Terminal whose form unloads, and tells the library, so a connection never writes to a freed control. Every handle belongs to the program that made it; whatever it leaves open is closed (a modem hangs up, a capture file is closed, a transfer cancelled) when it ends or is ended.

    Networking Library

    TCP/IP for DVX programs with the standard BSD socket API: a C programmer who knows sockets writes DVX network code the usual way. IPv4 today; the types and constants for IPv6 (AF_INET6, sockaddr_in6, sockaddr_storage) are defined so code can be written for both.

    -

    Loaded as: bin/libs/kpunch/dvxnet/dvxnet.lib (needs the shell, the task switcher and the core library)

    -

    Headers: dvxnet/dvxNet.h (settings and status), and the standard headers in dvxnet/include: sys/socket.h, netinet/in.h, netinet/tcp.h, arpa/inet.h, netdb.h, sys/select.h, poll.h, sys/ioctl.h and errno.h (the C library's plus the network codes). Put the include directory on the compiler's include path ahead of the C library's. In the SDK they are in include/net, and a project Makefile that sets NET = 1 gets them (see SDK Quick Start).

    +

    Loaded as: bin/libs/kpunch/dvxnet/dvxnet.lib (needs the shell, the task switcher, and the core library)

    +

    Headers: dvxnet/dvxNet.h (settings and status), and the standard headers in dvxnet/include: sys/socket.h, netinet/in.h, netinet/tcp.h, arpa/inet.h, netdb.h, sys/select.h, poll.h, sys/ioctl.h, and errno.h (the C library's plus the network codes). Put the include directory on the compiler's include path ahead of the C library's. In the SDK they are in include/net, and a project Makefile that sets NET = 1 gets them (see SDK Quick Start).

    The socket API

    -

    Waiting, polling and cooperative multitasking

    +

    Waiting, polling, and cooperative multitasking

    Settings and status (dvxNet.h)

    How it works

    Networking from BASIC

    @@ -7338,7 +7702,7 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

    The Socket API

    -

    Standard names, arguments and meanings. A socket is an ordinary file descriptor: read(), write() and close() work on it, as do fcntl(F_GETFL/F_SETFL, O_NONBLOCK), ioctl(FIONBIO, FIONREAD), select() and poll().

    +

    Standard names, arguments, and meanings. A socket is an ordinary file descriptor: read(), write(), and close() work on it, as do fcntl(F_GETFL/F_SETFL, O_NONBLOCK), ioctl(FIONBIO, FIONREAD), select(), and poll().

      Function                                   Notes
       --------                                   -----
       socket(AF_INET, SOCK_STREAM or SOCK_DGRAM, 0)  Starts the network with the saved settings the first time. EAFNOSUPPORT for other families (AF_INET6 included, for now).
    @@ -7355,7 +7719,7 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    getnameinfo Numeric host and service; NI_NAMEREQD gives EAI_NONAME. gethostname The computer name from the network settings. inet_pton, inet_ntop AF_INET (AF_INET6: EAFNOSUPPORT). - inet_addr, inet_aton, inet_ntoa BSD forms (a.b.c.d, a.b.c, a.b, a; decimal, octal or hex). + inet_addr, inet_aton, inet_ntoa BSD forms (a.b.c.d, a.b.c, a.b, a; decimal, octal, or hex). htonl, htons, ntohl, ntohs Inline in <netinet/in.h>; also real functions for code built without optimisation.

    Error Codes

    errno.h adds the network codes: EWOULDBLOCK (the same as EAGAIN), EINPROGRESS, EALREADY, ENOTSOCK, EDESTADDRREQ, EMSGSIZE, EPROTOTYPE, ENOPROTOOPT, EPROTONOSUPPORT, ESOCKTNOSUPPORT, EOPNOTSUPP (ENOTSUP), EPFNOSUPPORT, EAFNOSUPPORT, EADDRINUSE, EADDRNOTAVAIL, ENETDOWN, ENETUNREACH, ENETRESET, ECONNABORTED, ECONNRESET, ENOBUFS, EISCONN, ENOTCONN, ESHUTDOWN, ETIMEDOUT, ECONNREFUSED, EHOSTDOWN, EHOSTUNREACH. The C library's strerror() does not know them; netErrorText(errno) gives plain words for them.

    @@ -7363,19 +7727,19 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

    Sockets are real descriptors below FD_SETSIZE (256), at most 32 at a time. dup() and dup2() refuse socket descriptors (EOPNOTSUPP), lseek gives ESPIPE.

    -

    Waiting, Polling and Cooperative Multitasking

    -

    DVX runs programs cooperatively, so a call that has to wait never stops the system: blocking connect, accept, recv, send (with a full buffer), select, poll and name lookups wait by running the network and then letting the other tasks run (tsYield), again and again, until they can return. Other programs, and other connections, keep working meanwhile.

    +

    Waiting, Polling, and Cooperative Multitasking

    +

    DVX runs programs cooperatively, so a call that has to wait never stops the system: blocking connect, accept, recv, send (with a full buffer), select, poll, and name lookups wait by running the network and then letting the other tasks run (tsYield), again and again, until they can return. Other programs, and other connections, keep working meanwhile.

    The network also runs from the shell's idle time, so connections progress (and data arrives) while a program is busy with other things; every socket call runs it too. A program that polls non-blocking sockets in its own loop needs nothing else.

    -

    Right after the network starts, DHCP needs a moment to get an address. connect, sendto and name lookups wait for it (up to 20 seconds) instead of failing, so a program started together with DVX can connect at once.

    +

    Right after the network starts, DHCP needs a moment to get an address. connect, sendto, and name lookups wait for it (up to 20 seconds) instead of failing, so a program started together with DVX can connect at once.

    Sockets Belong to Programs

    Each socket belongs to the program that created it (accept gives the new socket to the program that accepts). When a program ends, normally or because it was ended or crashed, the shell closes every socket it still had open, and drops its unfinished name lookups.

    Settings and Status (dvxNet.h)

    -

    The settings live in DVX.INI, section [network]: dhcp (true/false), address, netmask, gateway, dns1, dns2, hostname and vector (auto or 0x60-0x80). The Control Panel's Network page edits them.

    +

    The settings live in DVX.INI, section [network]: dhcp (true/false), address, netmask, gateway, dns1, dns2, hostname, and vector (auto or 0x60-0x80). The Control Panel's Network page edits them.

      Function                                  Description
       --------                                  -----------
    -  int32_t netStart(void)                    Start with the saved settings unless running: NET_OK, NET_ERR_NO_DRIVER (the stack runs anyway, for 127.0.0.1), NET_ERR_CONFIG or NET_ERR_NO_MEMORY. Socket calls do this themselves.
    +  int32_t netStart(void)                    Start with the saved settings unless running: NET_OK, NET_ERR_NO_DRIVER (the stack runs anyway, for 127.0.0.1), NET_ERR_CONFIG, or NET_ERR_NO_MEMORY. Socket calls do this themselves.
       void netStop(void)                        Close every socket and stop (the packet driver is released).
       int32_t netRestart(void)                  Stop, then start with the saved settings (after they changed).
       void netPoll(void)                        Take received frames, run the timers, send. Called from the shell's idle time and by every socket call.
    @@ -7404,11 +7768,11 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    Settings dvxNet.c DVX.INI, status, errors in plain words.

    lwIP runs without threads (NO_SYS), configured only by port/lwipopts.h: TCP MSS 1460 with a 4-segment window, 16 TCP connections, 48 KB of stack memory, DHCP, DNS (2 servers), ARP, ICMP (ping answers), IP fragments, loopback. Nothing runs lwIP at interrupt time.

    The receive ring holds 16 frames. A burst larger than that while DVX is busy loses frames (counted as "lost" in the status); TCP sends them again.

    -

    On the host the same sources are built into the "net" test suite: a fake packet driver passes frames through the real ring to a second copy of the stack in another process, which plays DHCP, DNS and echo server.

    +

    On the host the same sources are built into the "net" test suite: a fake packet driver passes frames through the real ring to a second copy of the stack in another process, which plays DHCP, DNS, and echo server.

    Networking from BASIC

    -

    net.bas declares simple functions over the socket API (netBas.c): HostLookup$, HostName$, NetAddress$, NetError$, TcpConnect, TcpListen, TcpAccept, TcpSend, TcpPending, TcpRecv$, TcpConnected, TcpShutdown, TcpPeer$, TcpLocalPort, TcpSetNoDelay, TcpSetKeepAlive, TcpClose, UdpOpen, UdpSend, UdpRecv$, UdpFrom$, UdpClose. A handle is the socket's descriptor + 1, so 0 means failure. Only TcpConnect (up to 20 seconds) and TcpSend wait; everything else returns at once, so a program polls from a Timer. TcpSend, TcpRecv$, UdpSend and UdpRecv$ pass their data as BINARY STRING (pointer and length), so every byte value goes through. See the BASIC language reference, Include Libraries.

    +

    net.bas declares simple functions over the socket API (netBas.c): HostLookup$, HostName$, NetAddress$, NetError$, TcpConnect, TcpListen, TcpAccept, TcpSend, TcpPending, TcpRecv$, TcpConnected, TcpShutdown, TcpPeer$, TcpLocalPort, TcpSetNoDelay, TcpSetKeepAlive, TcpClose, UdpOpen, UdpSend, UdpRecv$, UdpFrom$, UdpClose. A handle is the socket's descriptor + 1, so 0 means failure. Only TcpConnect (up to 20 seconds) and TcpSend wait; everything else returns at once, so a program polls from a Timer. TcpSend, TcpRecv$, UdpSend, and UdpRecv$ pass their data as BINARY STRING (pointer and length), so every byte value goes through. See the BASIC language reference, Include Libraries.

      BASIC                                   C it calls
       -----                                   ----------
       HostName$(address)                      getnameinfo: the address itself, as this stack does not look names of addresses up; "" when it is not an address.
    @@ -7695,7 +8059,7 @@ shellConfigPath(ctx, "settings.ini", path, sizeof(path));
     
     void shellRegisterAppCleanup(ShellAppCleanupFnT fn);
     void shellUnregisterAppCleanup(ShellAppCleanupFnT fn);
    -

    Register a function the shell calls with an app's id whenever that app ends -- normally, force-killed or after a crash -- once its windows are gone, next to the shell's own release of the app's widget classes and hotkeys. A library uses it to release what the app left open with it: the networking library closes the app's sockets this way. For library code that stays loaded; registering the same function again has no effect, and unregistering one that is not registered does nothing.

    +

    Register a function the shell calls with an app's id whenever that app ends -- normally, force-killed, or after a crash -- once its windows are gone, next to the shell's own release of the app's widget classes and hotkeys. A library uses it to release what the app left open with it: the networking library closes the app's sockets this way. For library code that stays loaded; registering the same function again has no effect, and unregistering one that is not registered does nothing.

    shellDesktopUpdate

    void shellDesktopUpdate(void);

    Notify all registered desktop callbacks that app state has changed. Called internally by the shell after loading or reaping an app. Can also be called by apps that need to trigger a desktop refresh. Each call also adds one to the change counter (shellDesktopChangeCount).

    @@ -7705,7 +8069,7 @@ void shellUnregisterAppCleanup(ShellAppCleanupFnT fn);

    shellCtrlEscFn / shellOpenTaskManager

    extern void (*shellCtrlEscFn)(AppContextT *ctx);
     void shellOpenTaskManager(AppContextT *ctx);
    -

    shellCtrlEscFn is set by the taskmgr DXE's constructor; NULL if the task manager is not loaded. shellOpenTaskManager calls it when it is set and does nothing otherwise; Ctrl+Esc, Program Manager's Help > Task Manager and BASIC's ShellTaskManager all open Task Manager through it.

    +

    shellCtrlEscFn is set by the taskmgr DXE's constructor; NULL if the task manager is not loaded. shellOpenTaskManager calls it when it is set and does nothing otherwise; Ctrl+Esc, Program Manager's Help > Task Manager, and BASIC's ShellTaskManager all open Task Manager through it.

    System Information

    @@ -7721,7 +8085,7 @@ void shellOpenTaskManager(AppContextT *ctx);

    Installed Programs

    Header: dvxshell/shellProg.h

    -

    Finds the installed programs (every .app file under APPS, SHELL_PROG_DIR) and reads what they are called and what they look like, for launchers, file managers and setup programs. A program's display name and description are its "name" and "description" text resources (the capitalized file name when it has no name resource); its picture is its "icon32" resource.

    +

    Finds the installed programs (every .app file under APPS, SHELL_PROG_DIR) and reads what they are called and what they look like, for launchers, file managers, and setup programs. A program's display name and description are its "name" and "description" text resources (the capitalized file name when it has no name resource); its picture is its "icon32" resource.

      Constant                 Value      Meaning
       --------                 -----      -------
       SHELL_PROG_DIR           "apps"     Folder, relative to the DVX root, holding every program.
    @@ -7739,7 +8103,7 @@ void shellOpenTaskManager(AppContextT *ctx);

    Every program under dir (NULL for SHELL_PROG_DIR) and its subfolders, except skipPath (compared with platformPathSame; NULL for none), in the order found. With readInfo each entry is filled by shellProgramInfo; without it only path is set, which is fast (no program file is opened) and enough to notice programs being added or removed. progress, when not NULL, is called with progressCtx after each program (for example to keep the screen responsive). Returns the count; *outList is an array of that many entries, NULL when there are none. Free it with shellProgramListFree.

    shellProgramInfo

    void shellProgramInfo(const char *path, ShellProgramT *out);
    -

    Fill out with the program's path, display name and description. Always succeeds: without resources (or for a file that is not a program) the name is the file name without its extension, first letter capitalized, and the description is empty.

    +

    Fill out with the program's path, display name, and description. Always succeeds: without resources (or for a file that is not a program) the name is the file name without its extension, first letter capitalized, and the description is empty.

    shellProgramIcon

    uint8_t *shellProgramIcon(AppContextT *ctx, const char *path, int32_t edge, int32_t *outPitch);

    A picture edge x edge pixels in display format, for drawing with rectCopy: a program's icon32 resource, or any image file dvxLoadImage reads (BMP, PNG, JPEG, GIF). Other sizes are scaled (nearest pixel). Returns NULL when there is no picture. Free with dvxFreeImage.

    @@ -7831,7 +8195,7 @@ const char *ShellAssocValue(int32_t index); int32_t ShellAssocSet(const char *extension, const char *value); int32_t ShellAssocRemove(const char *extension); const char *ShellAssocNormalize(const char *extension); -

    ShellAssocProgram is shellAssocLookup: the stored value, the program followed by ",args" when there is a template. ShellAssocCommand is the program part of shellAssocCommand for a document. ShellAssocCount, ShellAssocExt and ShellAssocValue enumerate shellAssocGet (0-based; "" past the end). ShellAssocSet, ShellAssocRemove and ShellAssocNormalize are shellAssocSet, shellAssocRemove and shellAssocNormalize ("" when the extension cannot be one). Every change is saved at once.

    +

    ShellAssocProgram is shellAssocLookup: the stored value, the program followed by ",args" when there is a template. ShellAssocCommand is the program part of shellAssocCommand for a document. ShellAssocCount, ShellAssocExt, and ShellAssocValue enumerate shellAssocGet (0-based; "" past the end). ShellAssocSet, ShellAssocRemove, and ShellAssocNormalize are shellAssocSet, shellAssocRemove, and shellAssocNormalize ("" when the extension cannot be one). Every change is saved at once.

    Running programs and DVX

    int32_t     ShellRunningCount(void);
     int32_t     ShellChangeCount(void);
    @@ -7878,12 +8242,12 @@ int32_t     ShellSetWelcome(const char *program);
    const char *ShellItemIcon(const char *iconPath, const char *path);

    ShellProgramsFolder is SHELL_PROG_DIR (where ShellProgramScan("") looks and a Browse for a program starts). ShellItemIcon is shellItemIconFile ("" for none). The program-group bindings (ShellGroups*, ShellGroup*, ShellItem*) are in pmModelBas.c; see Program Groups.

    A desktop in BASIC

    -

    A BASIC program can be the desktop ([shell] desktop, ShellSetDesktop): the shell exits DVX when it ends, so it needs no code for that; its Exit command confirms and calls ShellExitDvx. Its groups use the shell's program-group model (Program Groups: the same PROGMAN.INI format, rules and limits as Program Manager) through shell.bas, in ShellConfigFile$'s folder. An icon grid is drawn on a PictureBox with PaintPicture and pictures from LOADPICTURE of ShellItemIcon$ (a .app gives its icon), with MouseDown/MouseUp for selecting and dragging and KeyDown for the keyboard. The test fixture src/test/dos/fixtures/parpm does all of this.

    +

    A BASIC program can be the desktop ([shell] desktop, ShellSetDesktop): the shell exits DVX when it ends, so it needs no code for that; its Exit command confirms and calls ShellExitDvx. Its groups use the shell's program-group model (Program Groups: the same PROGMAN.INI format, rules, and limits as Program Manager) through shell.bas, in ShellConfigFile$'s folder. An icon grid is drawn on a PictureBox with PaintPicture and pictures from LOADPICTURE of ShellItemIcon$ (a .app gives its icon), with MouseDown/MouseUp for selecting and dragging and KeyDown for the keyboard. The test fixture src/test/dos/fixtures/parpm does all of this.

    Program Groups

    Header: dvxshell/pmModel.h; source pmModel.c (model) and pmModelBas.c (BASIC entry points, shell.bas).

    -

    Program Manager's groups, items and PROGMAN.INI, as plain data with no windows, part of the shell so the C Program Manager and a desktop written in BASIC share one model (the pm prefix names it). groups[PM_INSTALLED_IDX] is the automatic Installed Programs group (pmInstalledScan: every program under SHELL_PROG_DIR, sorted by name, never saved; pmInstalledChanged tells cheaply whether a program was added or removed); the others are the user's, saved by pmModelSave with a crash-safe rewrite in this layout:

    +

    Program Manager's groups, items, and PROGMAN.INI, as plain data with no windows, part of the shell so the C Program Manager and a desktop written in BASIC share one model (the pm prefix names it). groups[PM_INSTALLED_IDX] is the automatic Installed Programs group (pmInstalledScan: every program under SHELL_PROG_DIR, sorted by name, never saved; pmInstalledChanged tells cheaply whether a program was added or removed); the others are the user's, saved by pmModelSave with a crash-safe rewrite in this layout:

    [options]  minimizeOnRun, restoreAlone, autoArrange, view, group
     [groups]   count
     [groupN]   name, count, itemM.name, itemM.program, itemM.args,
    @@ -7907,7 +8271,7 @@ const char *ShellItemIcon(const char *iconPath, const char *path);

    BASIC System Settings

    Source: dvxshell/shellSysBas.c; BASIC declarations: src/include/basic/system.bas (DECLARE LIBRARY "dvxshell"); prototypes in shellApp.h.

    -

    What Control Panel changes, for DVX BASIC programs: the video mode, the system colors and themes, the wallpaper and the mouse, each applied at once to shellContext(). Only BASIC-marshalable types; True (-1) / False (0); colors are BASIC RGB() values (&HBBGGRR, converted with wgtBasicColor); lists are 0-based. Arguments named prefs are prefs.bas handles, resolved with prefsBasHandle (dvxPrefs.h); 0 or a closed handle makes a Store or Load do nothing (a Load returns False).

    +

    What Control Panel changes, for DVX BASIC programs: the video mode, the system colors and themes, the wallpaper, text smoothing, and the mouse, each applied at once to shellContext(). Only BASIC-marshalable types; True (-1) / False (0); colors are BASIC RGB() values (&HBBGGRR, converted with wgtBasicColor); lists are 0-based. Arguments named prefs are prefs.bas handles, resolved with prefsBasHandle (dvxPrefs.h); 0 or a closed handle makes a Store or Load do nothing (a Load returns False).

    int32_t     SysVideoModeCount(void);
     int32_t     SysVideoModeWidth(int32_t index);
     int32_t     SysVideoModeHeight(int32_t index);
    @@ -7930,6 +8294,10 @@ void        SysSetWallpaperMode(int32_t mode);
     int32_t     SysWallpaperMode(void);
     void        SysWallpaperStore(int32_t prefs);
     int32_t     SysWallpaperLoad(int32_t prefs);
    +void        SysSetTextSmoothing(int32_t on);
    +int32_t     SysTextSmoothing(void);
    +void        SysFontsStore(int32_t prefs);
    +void        SysFontsLoad(int32_t prefs);
     void        SysSetMouse(int32_t wheelDir, int32_t dblClickMs, int32_t accel, int32_t mickeys, int32_t wheelStep);
     int32_t     SysMouseWheelDir(void);
     int32_t     SysMouseDoubleClick(void);
    @@ -7966,6 +8334,9 @@ int32_t     SysMemoryFreeKb(void);
    SysSetWallpaperMode dvxSetWallpaperMode with the value of prefsWallpaperChoices[mode]; other modes are ignored SysWallpaper / Mode ctx->wallpaperPath; the prefsWallpaperChoices index of ctx->wallpaperMode SysWallpaperStore / Load dvxWallpaperToPrefs / dvxWallpaperFromPrefs (Load: False when the image could not be loaded) + SysSetTextSmoothing dvxSetFontSmoothing (nonzero is on) + SysTextSmoothing ctx->fontSmoothing (the setting; text is smoothed only above 8 bits per pixel) + SysFontsStore / Load dvxFontsToPrefs / dvxFontsFromPrefs SysSetMouse dvxSetMouseConfig SysMouse* ctx->wheelDirection, dblClickMs, mouseAccel, mouseMickeys, wheelStep SysMouseStore / Load dvxMouseToPrefs / dvxMouseFromPrefs @@ -7977,7 +8348,7 @@ int32_t SysMemoryFreeKb(void); SysScreenshot dvxScreenshot (BMP for .bmp, else PNG) SysMemoryTotalKb / FreeKb platformGetMemoryInfo; -1 when unknown

    There is no binding for platformInjectMouse: on DOS it replaces the real mouse until the next call and has no way back to it, so a synthesized click would leave the pointer stuck. A program moves the pointer with SysSetMousePos and "clicks" a control by calling its event procedure or setting its value.

    -

    system.bas also holds MOUSE_WHEEL_STEP_MIN / MAX / DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS and MOUSE_SPEED_DEFAULT, and SYS_VIDEO_DEFAULT_WIDTH / HEIGHT / BPP (DVX_DEFAULT_VIDEO_W / H / BPP, the mode prefsGetVideoMode and safe mode fall back to), as CONSTs; the basdev test suite (caseIncludeConsts) checks each against dvxTypes.h.

    +

    system.bas also holds MOUSE_WHEEL_STEP_MIN / MAX / DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS, and MOUSE_SPEED_DEFAULT, and SYS_VIDEO_DEFAULT_WIDTH / HEIGHT / BPP (DVX_DEFAULT_VIDEO_W / H / BPP, the mode prefsGetVideoMode and safe mode fall back to), as CONSTs; the basdev test suite (caseIncludeConsts) checks each against dvxTypes.h.

    The Store / Load routines are the ones the shell uses at start (dvxMouseFromPrefs, dvxColorsFromPrefs, dvxWallpaperFromPrefs on DVX.INI) and Control Panel uses for OK and for its Cancel snapshot (an in-memory prefsCreate handle), so the INI keys are written in one place (dvxApp.c).

    @@ -7985,7 +8356,7 @@ int32_t SysMemoryFreeKb(void);

    High-level wrapper around SQLite3 for DVX applications. Manages database connections and result set cursors via integer handles so BASIC code never touches raw pointers. All handles are 1-based; 0 indicates an error or invalid handle.

    Header: sql/dvxSql.h

    SQLite Wrapping

    -

    dvxSql wraps the bundled SQLite 3.53.4 amalgamation (sql/thirdparty/sqlite3/sqlite3.c and sqlite3.h; origin, checksum and build options in sql/thirdparty/sqlite3/DVX-VERSION.md). It manages two internal dynamic tables keyed by 1-based handles:

    +

    dvxSql wraps the bundled SQLite 3.53.4 amalgamation (sql/thirdparty/sqlite3/sqlite3.c and sqlite3.h; origin, checksum, and build options in sql/thirdparty/sqlite3/DVX-VERSION.md). It manages two internal dynamic tables keyed by 1-based handles:

    • Database table: each slot holds a sqlite3 * plus a per-database error string and affected-row count.
    • Cursor table: each slot holds a sqlite3_stmt *, the owning database handle, and EOF tracking.
    • @@ -7995,8 +8366,8 @@ int32_t SysMemoryFreeKb(void);

      SQLite is compiled with the options listed in sql/thirdparty/sqlite3/sqliteOpts.mk (the host test build uses the same list). The ones that change what programs see:

      • Double-quoted text is always an identifier (SQLITE_DQS=0): write string literals in single quotes. SELECT "abc" fails with "no such column: abc".
      • -
      • The JSON functions, WAL journal mode, loadable extensions, shared cache, UTF-16 APIs and deprecated APIs are left out. PRAGMA journal_mode = WAL leaves the database in rollback-journal (DELETE) mode.
      • -
      • Temporary tables, sorts and statement journals are kept in memory (PRAGMA temp_store = FILE switches a connection to temporary files).
      • +
      • The JSON functions, WAL journal mode, loadable extensions, shared cache, UTF-16 APIs, and deprecated APIs are left out. PRAGMA journal_mode = WAL leaves the database in rollback-journal (DELETE) mode.
      • +
      • Temporary tables, sorts, and statement journals are kept in memory (PRAGMA temp_store = FILE switches a connection to temporary files).
      • New databases use 1 KB pages; each connection caches up to 512 KB. Existing databases keep their own page size.
      • LIKE never matches BLOB values.
      @@ -8006,10 +8377,10 @@ int32_t SysMemoryFreeKb(void);
    • Journals keep 8.3 names: DATA.DB uses DATA.NAL (the last three letters of "-journal"), and an extensionless DATA uses DATA.NAL too. Two databases that differ only in extension (DATA.DB and DATA.DAT) share the journal name, so keep them in different directories.
    • Paths are made absolute and canonical (relative names join the current directory, backslashes become forward slashes, "." and ".." are resolved), so every spelling of one file is the same database.
    • Locking is in-process: DOS is single user, but two connections inside DVX (two programs, or a Data control and BASIC code) can share a file. Any number may read; one may write. A write while another connection is writing, or a commit while another connection has a query open, fails with "database is locked"; retry after the other side finishes (dvxSqlFreeResult releases a query's read lock). Nothing is locked at the DOS level, so do not share a database with another program outside DVX.
    • -
    • Temporary files, when used, are SQLxxxxx.TMP in the directory named by TMPDIR, TEMP or TMP (else the current directory) and are deleted when closed.
    • +
    • Temporary files, when used, are SQLxxxxx.TMP in the directory named by TMPDIR, TEMP, or TMP (else the current directory) and are deleted when closed.
    • Randomness comes from the serial library's generator (secRngBytes) when that library is loaded, else from platformEntropyStamp mixed with the time of day.
    -

    Databases written by the SQLite 3.5.6 that earlier DVX releases bundled open, read and write unchanged. Files created now use the current file format, which SQLite 3.5.6 cannot read.

    +

    Databases written by the SQLite 3.5.6 that earlier DVX releases bundled open, read, and write unchanged. Files created now use the current file format, which SQLite 3.5.6 cannot read.

    Handle Model

    Database and cursor handles are int32_t values. A successful open or query returns a handle greater than zero. Handle 0 is reserved as the invalid/error sentinel. Closing a database automatically finalizes all cursors that belong to it, so callers do not need to track cursor lifetimes per-database.

    Connection Lifecycle

    @@ -8060,7 +8431,7 @@ int32_t SysMemoryFreeKb(void);
      Parameter    Description
       ---------    -----------
       db           Database handle.
    -

    Returns the error string, or "Invalid database handle" if the handle is invalid. Messages are SQLite's own (for example "no such table: x" or "database is locked"); when a file could not be opened, read or written, the operating system's reason follows in parentheses. Handle 0 (what a failed dvxSqlOpen returns) reports why the most recent open failed; after a successful open it is invalid again.

    +

    Returns the error string, or "Invalid database handle" if the handle is invalid. Messages are SQLite's own (for example "no such table: x" or "database is locked"); when a file could not be opened, read, or written, the operating system's reason follows in parentheses. Handle 0 (what a failed dvxSqlOpen returns) reports why the most recent open failed; after a successful open it is invalid again.

    dvxSqlAffectedRows

    int32_t dvxSqlAffectedRows(int32_t db);

    Return the number of rows inserted, updated, or deleted by the last dvxSqlExec call on this handle.

    @@ -8202,23 +8573,23 @@ dvxSqlExec(db, sql);

    File Operations Library

    -

    Attributes, folder listing, path helpers and recursive copy/move/delete jobs with progress, Cancel and conflict handling. The File Manager and Setup are user interfaces over this library; any program can use it. Drives, free space and wildcard filters come from the platform layer (dvxPlat.h): platformDriveList, platformDiskSpace and platformFilterMatch.

    +

    Attributes, folder listing, path helpers, and recursive copy/move/delete jobs with progress, Cancel, and conflict handling. The File Manager and Setup are user interfaces over this library; any program can use it. Drives, free space, and wildcard filters come from the platform layer (dvxPlat.h): platformDriveList, platformDiskSpace, and platformFilterMatch.

    Header: fileops/fileOps.h

    Loaded as: bin/libs/kpunch/fileops/fileops.lib

    DOS Disk Errors

    On DOS, touching a drive with no disk (or a write-protected or removed one) raises a critical error, which would normally wait at an "Abort, Retry, Fail" prompt on the hidden text screen. DVX installs a critical-error handler at startup (platformInit) that answers Fail, so every call here, and your own disk code, simply gets an error (errno) instead. platformDriveList never touches the media at all (IOCTL calls only), so listing drives costs nothing even with empty drives.

    Big Disks and Long File Names

    -

    Disks past 2 GB report their real size and free space: platformDiskSpace asks DOS with the FAT32 call first and falls back to the older one, which stops near 2 GB. File sizes are 64-bit (FileOpsEntryT.size, fileOpsTreeSize, the job byte counts); a FAT32 file reaches 4 GB - 1 byte, and platformStatSize reads it correctly where stat's st_size would be negative. Where a long-name driver is running (Windows, DOSLFN) and covers the drive (platformLongNames), listings show long names, jobs copy and move them, attributes and renames work on them, fileOpsValidateName accepts them and fileOpsPathNormalize keeps their case; elsewhere the 8.3 rules apply.

    +

    Disks past 2 GB report their real size and free space: platformDiskSpace asks DOS with the FAT32 call first and falls back to the older one, which stops near 2 GB. File sizes are 64-bit (FileOpsEntryT.size, fileOpsTreeSize, the job byte counts); a FAT32 file reaches 4 GB - 1 byte, and platformStatSize reads it correctly where stat's st_size would be negative. Where a long-name driver is running (Windows, DOSLFN) and covers the drive (platformLongNames), listings show long names, jobs copy and move them, attributes and renames work on them, fileOpsValidateName accepts them, and fileOpsPathNormalize keeps their case; elsewhere the 8.3 rules apply.

    Attributes and Dates

      Function                                     Description
       --------                                     -----------
       fileOpsGetAttr(path, &attr)                  FILEOPS_ATTR_READONLY/HIDDEN/SYSTEM/ARCHIVE bits. Elsewhere than DOS: read-only = no write permission, hidden = a dot-file.
       fileOpsSetAttr(path, attr)                   Set the FILEOPS_ATTR_EDITABLE bits (elsewhere than DOS only read-only applies).
       fileOpsSetTime(path, mtime)                  Set a file's modification time.
    -

    Listing, Names and Paths

    +

    Listing, Names, and Paths

      Function                                              Description
       --------                                              -----------
    -  fileOpsListDir(dir, filter, &entries, &count)         Read a folder into an FileOpsEntryT array (name, size, mtime, isDir). Files not matching filter (platformFilterMatch: wildcards * and ?, several patterns separated by ';', ',' or blanks; "*.*" and an empty filter match everything) are left out; folders always stay. Free with fileOpsListFree.
    +  fileOpsListDir(dir, filter, &entries, &count)         Read a folder into an FileOpsEntryT array (name, size, mtime, isDir). Files not matching filter (platformFilterMatch: wildcards * and ?, several patterns separated by ';', ',', or blanks; "*.*" and an empty filter match everything) are left out; folders always stay. Free with fileOpsListFree.
       fileOpsListSort(entries, count, col, descending)      Folders first, then by FileOpsSortNameE/SizeE/TypeE/DateE; ties by name.
       fileOpsExtension(name)                                Extension without the dot, "" when none.
       fileOpsTypeName(entry, out, size)                     "Folder", "Program" (.APP), "TXT File", "File".
    @@ -8231,12 +8602,12 @@ dvxSqlExec(db, sql);
    fileOpsValidateName(dir, name, err, errSize) Check a new name for folder dir by the rules of its disk (platformValidateFilename): on DOS long names where the drive takes them (platformLongNames), else 8.3; never a device name. fileOpsPathJoin(out, size, dir, name) dir + separator + name in native separators (platformPathJoin: no doubled separator; an absolute name stands alone); false when it does not fit. fileOpsPathParent(path, out, size) Parent folder, in native separators; false at a root. - fileOpsPathIsRoot(path) True for "C:\", "C:" or "/". + fileOpsPathIsRoot(path) True for "C:\", "C:", or "/". fileOpsPathIsInside(path, dir) True when path is dir or below it (platformPathIsUnder). fileOpsPathSame(a, b) Same place (platformPathSame: separators equal, case ignored, a trailing separator ignored). fileOpsPathNormalize(path, out, size) Canonical absolute form of an existing folder (DOS: upper case, except names on a disk with long names, which keep their case).

    Jobs

    -

    A job copies, moves or deletes any number of files and folders (recursively):

    +

    A job copies, moves, or deletes any number of files and folders (recursively):

    FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, "C:\\BACKUP");
     
     fileOpsJobAddSource(job, "C:\\DOCS\\LETTER.TXT");
    @@ -8264,8 +8635,8 @@ fileOpsJobFree(job);
      Step result              Meaning
       -----------              -------
       FileOpsStepBusyE         More work to do.
    -  FileOpsStepAskE          A destination file exists, or a file to delete is read-only. Answer Yes, No, All or Cancel.
    -  FileOpsStepErrorE        Something failed; fileOpsJobMessage says why. Answer Retry, Skip or Cancel.
    +  FileOpsStepAskE          A destination file exists, or a file to delete is read-only. Answer Yes, No, All, or Cancel.
    +  FileOpsStepErrorE        Something failed; fileOpsJobMessage says why. Answer Retry, Skip, or Cancel.
       FileOpsStepDoneE         Finished.
       FileOpsStepCancelledE    Stopped by Cancel.

    Until it is answered, the job keeps returning the same question. What the job does:

    @@ -8280,12 +8651,12 @@ fileOpsJobFree(job);

    fileOpsJobClearAttr(job, attr), called before the first step, clears those attribute bits on every file a copy or move writes: Setup passes FILEOPS_ATTR_READONLY, because every file on a CD-ROM is read-only and the installed copies must not be.

    fileOpsJobRun(job, callbacks) runs a job to the end instead: progress(job, ctx) is called after every step (return false to cancel) and ask(job, kind, message, ctx) answers questions. With no callbacks, existing files are kept (No) and failing items are skipped.

    BASIC

    -

    src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsPathNative$ (platformPathNative), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename for a name in a folder), FileOpsValidLongName and FileOpsValidShortName (platformValidateLongName / platformValidateShortName, the rules alone), FileOpsLongNames (platformLongNames), FileOpsShortPath$ (platformShortPath, "" when it does not fit), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$) and jobs (FileOpsJobCreate, FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value.

    -

    Listings and jobs are handles (small numbers, 0 = none) into a table owned by the library; each records the app that made it, and the shell's app-cleanup hook frees what an app leaves open when it ends. FileOpsJobRunFor(job, ms) steps the job until the time is used or it needs an answer, so a BASIC Timer can run it without blocking the program. A returned string stays valid until the next call returning one (the BASIC runtime copies it at once). Single files are handled by the built-in FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR and SETATTR statements.

    +

    src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes, and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsPathNative$ (platformPathNative), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename for a name in a folder), FileOpsValidLongName and FileOpsValidShortName (platformValidateLongName / platformValidateShortName, the rules alone), FileOpsLongNames (platformLongNames), FileOpsShortPath$ (platformShortPath, "" when it does not fit), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$), and jobs (FileOpsJobCreate, FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value.

    +

    Listings and jobs are handles (small numbers, 0 = none) into a table owned by the library; each records the app that made it, and the shell's app-cleanup hook frees what an app leaves open when it ends. FileOpsJobRunFor(job, ms) steps the job until the time is used or it needs an answer, so a BASIC Timer can run it without blocking the program. A returned string stays valid until the next call returning one (the BASIC runtime copies it at once). Single files are handled by the built-in FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR, and SETATTR statements.

    Game Helpers Library

    -

    Frame pacing, held keys, program-owned timers and nearest-colour lookup for programs that run like games in a DVX window. It builds on the core's game input windows, held-key table, timer service and high-resolution clock (see Writing Games for DVX).

    +

    Frame pacing, held keys, program-owned timers, and nearest-colour lookup for programs that run like games in a DVX window. It builds on the core's game input windows, held-key table, timer service, and high-resolution clock (see Writing Games for DVX).

    Header: game/game.h

    Loaded as: bin/libs/kpunch/game/game.lib

    Frame Pacing

    @@ -8318,14 +8689,14 @@ while (running) {

    Help File Library

    -

    Reads compiled DVX help files (.hlp, written by the help compiler dvxhlpc or HelpCompile): topics, the table of contents, the keyword index, each topic's content records in order, pictures and full-text search. The Help Viewer, the HelpView widget (BASIC HelpPane) and BASIC's hlpfile.bas all read help files through it, so they find, search and show topics the same way.

    +

    Reads compiled DVX help files (.hlp, written by the help compiler dvxhlpc or HelpCompile): topics, the table of contents, the keyword index, each topic's content records in order, pictures, and full-text search. The Help Viewer, the HelpView widget (BASIC HelpPane), and BASIC's hlpfile.bas all read help files through it, so they find, search, and show topics the same way.

    Header: hlpfile/hlpFile.h (the file format: hlpfile/hlpFormat.h, shared with the help compiler)

    Loaded as: bin/libs/kpunch/hlpfile/hlpfile.lib

    Opening and Topics

    A file is an opaque HlpFileT. Strings returned point into the file's string table and stay valid until hlpFileClose.

      Function                                      Description
       --------                                      -----------
    -  hlpFileOpen(path)                             Open a help file; NULL when it cannot be read or is not a help file. The header, topic directory, strings, table of contents, index and search index are read at once; topic content and pictures are read when asked for.
    +  hlpFileOpen(path)                             Open a help file; NULL when it cannot be read or is not a help file. The header, topic directory, strings, table of contents, index, and search index are read at once; topic content and pictures are read when asked for.
       hlpFileClose(f)                               Close it (NULL is ignored).
       hlpFileTopicCount(f)                          Number of topics.
       hlpFileTopicId(f, topic) / Title(f, topic)    A topic's ID (what links and HelpTopic name) and title; "" out of range.
    @@ -8335,7 +8706,7 @@ while (running) {
       hlpFileTocCount(f) / hlpFileToc(f, i)         Table of contents entries (HlpTocEntryT: title string, topic or HLP_NO_TOPIC for a section heading, depth, HLP_TOC_EXPANDED flag).
       hlpFileIndexCount(f) / hlpFileIndex(f, i)     Keyword index entries (HlpIndexEntryT: keyword string, topic), sorted.

    Records and Pictures

    -

    hlpFileForEachRecord(f, topic, fn, userData) calls fn(hdr, payload, userData) for each record of a topic in order (HLP_REC_TEXT, HEADING1 .. 3, LIST_ITEM, NOTE with its HLP_NOTE_ kind in hdr->flags, CODE, TABLE, IMAGE with its HLP_IMG_ alignment, LINK, HRULE); the payload is NUL-terminated. fn returns false to stop. hlpRecordLink gives a link record's target topic ID and display text (the target when the text is empty); hlpRecordImage its picture's place in the file, and hlpFileReadImage the picture itself: the image file as the compiler took it in (BMP, PNG, JPEG or GIF), malloc'd, for dvxLoadImageAlpha. Reading a picture inside the record walk is safe.

    +

    hlpFileForEachRecord(f, topic, fn, userData) calls fn(hdr, payload, userData) for each record of a topic in order (HLP_REC_TEXT, HEADING1 .. 3, LIST_ITEM, NOTE with its HLP_NOTE_ kind in hdr->flags, CODE, TABLE, IMAGE with its HLP_IMG_ alignment, LINK, HRULE); the payload is NUL-terminated. fn returns false to stop. hlpRecordLink gives a link record's target topic ID and display text (the target when the text is empty); hlpRecordImage its picture's place in the file, and hlpFileReadImage the picture itself: the image file as the compiler took it in (BMP, PNG, JPEG, or GIF), malloc'd, for dvxLoadImageAlpha. Reading a picture inside the record walk is safe.

    hlpWrapText(text, cols) wraps flowing text at cols columns (at least HLP_MIN_WRAP_COLS) as the viewer does, a malloc'd copy with '\n' at the wrap points.

    Search

    hlpFileSearch(f, query, matches) finds the topics whose title or text contains every word of query (any case, separated by blanks), plus those with an index keyword containing the whole query, setting matches[topic] (hlpFileTopicCount bytes) and returning the count (-1 on a failure). The query is not changed. The compiler's trigram index narrows the candidates first; the trigram characters (hlpTrigramSymbol) and the record types searched (hlpRecordSearchable) are defined once in hlpFormat.h for both. Words with no three letters or digits in a row cannot use the index, so then only titles and keywords are searched.

    @@ -8352,7 +8723,7 @@ free(matches);

    BASIC

    -

    src/include/basic/hlpfile.bas declares the BASIC functions (hlpFileBas.c): HlpOpen, HlpClose, HlpTopicCount, HlpTopicId$, HlpTopicTitle$, HlpFindTopic, HlpStartTopic, HlpTocCount, HlpTocTitle$, HlpTocTopic, HlpTocDepth, HlpTocExpanded, HlpIndexCount, HlpIndexKeyword$, HlpIndexTopic, HlpSearch, HlpSearchTopic, HlpLoadTopic, HlpRecordType, HlpRecordFlags, HlpRecordText$, HlpRecordLink$, HlpRecordImageSave and HlpWrap$, with the HLP_REC_, HLP_NOTE_ and HLP_IMG_ constants of hlpFormat.h.

    +

    src/include/basic/hlpfile.bas declares the BASIC functions (hlpFileBas.c): HlpOpen, HlpClose, HlpTopicCount, HlpTopicId$, HlpTopicTitle$, HlpFindTopic, HlpStartTopic, HlpTocCount, HlpTocTitle$, HlpTocTopic, HlpTocDepth, HlpTocExpanded, HlpIndexCount, HlpIndexKeyword$, HlpIndexTopic, HlpSearch, HlpSearchTopic, HlpLoadTopic, HlpRecordType, HlpRecordFlags, HlpRecordText$, HlpRecordLink$, HlpRecordImageSave, and HlpWrap$, with the HLP_REC_, HLP_NOTE_, and HLP_IMG_ constants of hlpFormat.h.

    A handle (a small number, 0 = none) is an open file plus what the program last asked of it -- HlpSearch keeps the topics found for HlpSearchTopic, HlpLoadTopic copies a topic's records for the HlpRecord functions -- since BASIC cannot take a callback or a byte array. Each handle records the program that opened it; the shell's app-cleanup hook closes what a program leaves open when it ends. hlpFileBasHandle(handle) gives the HlpFileT behind a handle to C code (the HelpPane control's ShowTopic uses it). A returned string stays valid until the next call returning one (the BASIC runtime copies it at once).

    @@ -8370,9 +8741,9 @@ free(matches); firing = (st.buttons & 1) != 0; } } -

    joyCount() is the number of devices (the first call of any joystick function finds them); joyGetInfo(device, &info) describes one: name, id (the driver's name and unit number, "gameport0", which prefixes the device's DVX.INI keys), axisCount, buttonCount, hatCount, axisPresent (bit n set when axis n answers; a missing axis reads 0) and caps (JOY_CAP_CALIBRATE for analog axes, JOY_CAP_KEYBOARD for the keyboard joystick).

    +

    joyCount() is the number of devices (the first call of any joystick function finds them); joyGetInfo(device, &info) describes one: name, id (the driver's name and unit number, "gameport0", which prefixes the device's DVX.INI keys), axisCount, buttonCount, hatCount, axisPresent (bit n set when axis n answers; a missing axis reads 0), and caps (JOY_CAP_CALIBRATE for analog axes, JOY_CAP_KEYBOARD for the keyboard joystick).

    joyOpen(ctx, device) returns a handle owned by ctx->currentAppId, or -1. joyOpenShared(ctx, device) returns the calling program's shared handle on the device, opening it the first time (for code with no place to keep a handle; the BASIC functions use it). joyClose closes a handle; a program's handles are closed for it when it ends.

    -

    joyRead(handle, &st) fills a JoyStateT: axes[] (JOY_AXIS_MIN..JOY_AXIS_MAX, 0 inside the dead zone), hats[] (JOY_HAT_CENTERED or degrees clockwise from up), buttons (bit n = button n) and connected (false when the device stopped answering; everything then reads neutral). A button that went down and up again since the handle last looked reads as down once, so a quick tap between two reads is not lost. joyButtonPressed(handle, button) asks the same about one button. joyButtonWentDown(handle, button) only asks whether the button went down since the last time this function asked about it (a press counter of its own, apart from joyRead's).

    +

    joyRead(handle, &st) fills a JoyStateT: axes[] (JOY_AXIS_MIN..JOY_AXIS_MAX, 0 inside the dead zone), hats[] (JOY_HAT_CENTERED or degrees clockwise from up), buttons (bit n = button n), and connected (false when the device stopped answering; everything then reads neutral). A button that went down and up again since the handle last looked reads as down once, so a quick tap between two reads is not lost. joyButtonPressed(handle, button) asks the same about one button. joyButtonWentDown(handle, button) only asks whether the button went down since the last time this function asked about it (a press counter of its own, apart from joyRead's).

    The keyboard joystick (JOY_CAP_KEYBOARD) reads neutral for a handle whose program has no window with the focus, so keys typed into another program do not steer the game.

    Events

    joySetEventFn(handle, fn, userData) delivers the device's changes from now on: JoyEventAxisE when an axis moved by at least JOY_EVENT_AXIS_STEP or reached the centre or an end, JoyEventButtonDownE / JoyEventButtonUpE (a tap between two polls gives both), JoyEventHatE. fn runs in program context as the handle's program (ctx->currentAppId is its id), so DVX calls are fine. Events are delivered by joyPump, which the shell calls on every idle frame; a program with its own main loop that repaints every frame (so the shell is never idle) calls joyPump itself or reads with joyRead. joyPump is not reentrant: an event function that runs a modal dialog gets its remaining events after it returns. A NULL fn stops the events.

    @@ -8393,12 +8764,12 @@ free(matches);

    Two settings are offered as lists: joyChoices(which, &count) returns the choices of JoyChoiceGamePortE (gameport.mode: "Two joysticks", "One joystick with 4 axes") or JoyChoiceFallbackE (fallback: "When no joystick is found", "Always", "Never") in list order, each a PrefsChoiceT (dvxPrefs.h) with its label for people and the name stored; joyChoiceGet(which) is the index of the current one (the default for a missing or unknown value) and joyChoiceSet(which, index) stores one. The keyboard joystick's keys go by slot (0 to JOY_KBD_SLOT_COUNT - 1 in JOY_KBD_SLOT_NAMES order: up, down, left, right, button1 to button4): joyKbdSlotGet(slot) is the joyKeyName index of the slot's key (its default key when the setting names no known key), joyKbdSlotSet(slot, keyIndex) stores one; the keyboard driver reads its keys through joyKbdSlotGet. joyKeyIndex(name) finds a key name's index (joyKeyFind its raw code). The Control Panel's Game Controllers page uses these, so the stored names and the defaults live only in the library.

    joyRescan() looks for devices again (a joystick plugged in later, a changed gameport.mode); it works only while no handle is open and returns -1 otherwise. joyRescanApp(appId) closes every handle of that program first (joyAppCleanup) and then rescans, for a program that holds handles itself (the BASIC JoyRescan; the Control Panel closes its one handle and calls joyRescan).

    The Game Port Driver

    -

    joyport.drv drives the standard PC game port at I/O port 201h. Writing to the port starts four one-shot timers, one per axis; each runs for a time proportional to its axis's resistance (about 24 us plus 11 us per kilohm). The driver times them against the platform's microsecond clock (platformMicros) with interrupts left enabled, so the port never holds up the timer, the sound card or the mouse: each sample reads the port and the clock, and an axis whose fall can only be placed in a window wider than JOYPORT_GAP_US (40 us; an interrupt ran in between) keeps its previous value for that poll. A measurement gives up after JOYPORT_TIMEOUT_US (3 ms). At the probe an axis that does not run out, or runs out in under 10 us (some cards read 0 for an unconnected axis), is missing. At run time an axis that times out three polls in a row is left out of the measurement, so an unplugged stick does not cost the timeout every poll, and is tried again every 60 polls; a stick all of whose axes time out reads as not connected. Buttons A1, A2, B1, B2 are read at every poll and sampled by the timer interrupt, so short presses are latched.

    +

    joyport.drv drives the standard PC game port at I/O port 201h. Writing to the port starts four one-shot timers, one per axis; each runs for a time proportional to its axis's resistance (about 24 us plus 11 us per kilohm). The driver times them against the platform's microsecond clock (platformMicros) with interrupts left enabled, so the port never holds up the timer, the sound card, or the mouse: each sample reads the port and the clock, and an axis whose fall can only be placed in a window wider than JOYPORT_GAP_US (40 us; an interrupt ran in between) keeps its previous value for that poll. A measurement gives up after JOYPORT_TIMEOUT_US (3 ms). At the probe an axis that does not run out, or runs out in under 10 us (some cards read 0 for an unconnected axis), is missing. At run time an axis that times out three polls in a row is left out of the measurement, so an unplugged stick does not cost the timeout every poll, and is tried again every 60 polls; a stick all of whose axes time out reads as not connected. Buttons A1, A2, B1, B2 are read at every poll and sampled by the timer interrupt, so short presses are latched.

    In the default mode the port makes up to two joysticks (A with axes 0-1 and buttons 0-1, B with axes 2-3 and buttons 2-3; either may be missing); in the four-axis mode one joystick with four axes and four buttons.

    The Keyboard Joystick

    joykeys.drv is a fallback driver: a joystick made of keys (two axes: full deflection while a key is held, the centre with both or neither; four buttons), read from the held-key table, so games work without joystick hardware. It is listed only when no other device is found, unless [joystick] fallback says always or never.

    Writing a Driver

    -

    A driver is a driver module (see dvxDrv.h in the API reference): a file <name>.drv in bin/libs/kpunch/joystick/drivers, built by the joystick Makefile from each drivers/<name>/ folder, that exports one JoyDriverT named joyDriver, written with JOY_DRIVER(name, priority, .flags = ..., .probe = ..., .info = ..., .poll = ..., .tick = ..., .settingsChanged = ...). Nothing lists drivers and nothing loads them at startup: the first time a program asks for devices (the GUI and the timer service are running by then), and at every joyRescan, the library loads them all, probes them from the highest priority down (JOY_PRIORITY_PORT, ..., JOY_PRIORITY_KEYBOARD), probes the fallback drivers only when the fallback setting asks for them, and unloads every driver that found no device. The descriptor carries DVX_ABI_VERSION; a driver built for another version, one without probe, info or poll, or one whose name a higher priority driver already has is skipped and logged.

    +

    A driver is a driver module (see dvxDrv.h in the API reference): a file <name>.drv in bin/libs/kpunch/joystick/drivers, built by the joystick Makefile from each drivers/<name>/ folder, that exports one JoyDriverT named joyDriver, written with JOY_DRIVER(name, priority, .flags = ..., .probe = ..., .info = ..., .poll = ..., .tick = ..., .settingsChanged = ...). Nothing lists drivers and nothing loads them at startup: the first time a program asks for devices (the GUI and the timer service are running by then), and at every joyRescan, the library loads them all, probes them from the highest priority down (JOY_PRIORITY_PORT, ..., JOY_PRIORITY_KEYBOARD), probes the fallback drivers only when the fallback setting asks for them, and unloads every driver that found no device. The descriptor carries DVX_ABI_VERSION; a driver built for another version, one without probe, info, or poll, or one whose name a higher priority driver already has is skipped and logged.

      Member            Meaning
       ------            -------
       hdr.name          Lower case, no blanks ("gameport"); the prefix of the device ids and of the driver's own [joystick] keys.
    @@ -8406,22 +8777,22 @@ free(matches);
    flags JOY_DRIVER_FALLBACK for a stand-in like the keyboard joystick. probe() Find the units; return how many. Called right after the driver is loaded. info(unit, out) Describe one unit (name, counts, axisPresent, caps); the library fills in id. - poll(raw, units) Read every unit now, in program context: raw[0..units-1]. Set axisValid for each axis measured (the others keep their last value), buttons held now, pressed for buttons that went down since the last poll, hats (JOY_HAT_CENTERED when not pushed) and connected. Axes are raw units with JOY_CAP_CALIBRATE, else JOY_AXIS_MIN..JOY_AXIS_MAX. + poll(raw, units) Read every unit now, in program context: raw[0..units-1]. Set axisValid for each axis measured (the others keep their last value), buttons held now, pressed for buttons that went down since the last poll, hats (JOY_HAT_CENTERED when not pushed), and connected. Axes are raw units with JOY_CAP_CALIBRATE, else JOY_AXIS_MIN..JOY_AXIS_MAX. tick() NULL, or called INSIDE THE TIMER INTERRUPT at the poll rate while a handle is open. The timer service's rules apply; the driver pins the code and data it touches with platformLockRegion and releases them with platformUnlockRegion when its module is unloaded (a destructor), so rescans do not pile up locked memory. settingsChanged() NULL, or re-read the driver's settings (joySettingGet) after they changed.

    Up to JOY_MAX_DRIVERS drivers in use get their tick called. A driver is loaded again at every probe, so it must not rely on state kept from an earlier load; set its hardware up in probe (or a constructor).

    BASIC

    -

    src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton and JoyHat. Joystick, axis, button and hat numbers start at 1; each function reads through the calling program's shared handle.

    -

    For a settings program (the Control Panel's Game Controllers page) it also declares: JoyHats, JoyAxisPresent(joy, axis), JoyCanCalibrate(joy) (JoyInfoT hatCount, axisPresent, JOY_CAP_CALIBRATE); JoyConnected(joy) (joyRead's connected); JoyRescan (joyRescanApp for the calling program: its own handles and STICK readings go first; -1 while another program has a joystick open); JoyRawAxis(joy, axis) and JoyRawButtons(joy) (joyGetRaw); JoySetCalibration(joy, axis, minV, centerV, maxV) and JoyClearCalibration(joy, axis) (joySetCalibration); JoyDeadZone / JoySetDeadZone (joyGetDeadZone / joySetDeadZone); JoyChoiceCount(which), JoyChoiceLabel$(which, index), JoyChoice(which), JoySetChoice(which, index) with JOY_CHOICE_GAMEPORT = 0 and JOY_CHOICE_FALLBACK = 1 (JoyChoiceE; joyChoices / joyChoiceGet / joyChoiceSet); JoyKeyCount and JoyKeyName$(index); JoyKbdSlotCount, JoyKbdSlot(slot), JoySetKbdSlot(slot, keyIndex) (joyKbdSlotGet / Set); JoySetting$(key, defaultValue) and JoySetSetting(key, value) ("" removes; joySettingGet / Set); JoySettingsLoad(prefs) and JoySettingsStore(prefs) with a prefs.bas handle (prefsBasHandle); JoyCalibration(joy, axis, part) (joyGetCalibration; part JOY_CAL_MIN / JOY_CAL_CENTER / JOY_CAL_MAX, joystick.h; 0 when joyGetCalibration fails); JoyIsKeyboard(joy) (JOY_CAP_KEYBOARD); JoyKeyIndex(keyName), JoyKeyCode(index), JoyKeyNameOf$(rawCode) (joyKeyIndex / joyKeyCode / joyKeyNameOf). CONSTs JOY_DEADZONE_DEFAULT / _MAX, JOY_POLL_HZ_DEFAULT / _MIN / _MAX and JOY_KEY_POLL_HZ mirror joystick.h. Lists (choices, keys, slots) are numbered from 0; True is -1.

    +

    src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton, and JoyHat. Joystick, axis, button, and hat numbers start at 1; each function reads through the calling program's shared handle.

    +

    For a settings program (the Control Panel's Game Controllers page) it also declares: JoyHats, JoyAxisPresent(joy, axis), JoyCanCalibrate(joy) (JoyInfoT hatCount, axisPresent, JOY_CAP_CALIBRATE); JoyConnected(joy) (joyRead's connected); JoyRescan (joyRescanApp for the calling program: its own handles and STICK readings go first; -1 while another program has a joystick open); JoyRawAxis(joy, axis) and JoyRawButtons(joy) (joyGetRaw); JoySetCalibration(joy, axis, minV, centerV, maxV) and JoyClearCalibration(joy, axis) (joySetCalibration); JoyDeadZone / JoySetDeadZone (joyGetDeadZone / joySetDeadZone); JoyChoiceCount(which), JoyChoiceLabel$(which, index), JoyChoice(which), JoySetChoice(which, index) with JOY_CHOICE_GAMEPORT = 0 and JOY_CHOICE_FALLBACK = 1 (JoyChoiceE; joyChoices / joyChoiceGet / joyChoiceSet); JoyKeyCount and JoyKeyName$(index); JoyKbdSlotCount, JoyKbdSlot(slot), JoySetKbdSlot(slot, keyIndex) (joyKbdSlotGet / Set); JoySetting$(key, defaultValue) and JoySetSetting(key, value) ("" removes; joySettingGet / Set); JoySettingsLoad(prefs) and JoySettingsStore(prefs) with a prefs.bas handle (prefsBasHandle); JoyCalibration(joy, axis, part) (joyGetCalibration; part JOY_CAL_MIN / JOY_CAL_CENTER / JOY_CAL_MAX, joystick.h; 0 when joyGetCalibration fails); JoyIsKeyboard(joy) (JOY_CAP_KEYBOARD); JoyKeyIndex(keyName), JoyKeyCode(index), JoyKeyNameOf$(rawCode) (joyKeyIndex / joyKeyCode / joyKeyNameOf). CONSTs JOY_DEADZONE_DEFAULT / _MAX, JOY_POLL_HZ_DEFAULT / _MIN / _MAX, and JOY_KEY_POLL_HZ mirror joystick.h. Lists (choices, keys, slots) are numbered from 0; True is -1.

    The BASIC runtime calls joyBasStick(n) and joyBasStrig(n) for QBasic's STICK and STRIG. Joystick A is device 0 (axes 0-1, buttons 0 and 1); joystick B is device 1, or axes 2-3 and buttons 2-3 of device 0 when it is the only device and has more than two axes. STICK(0) reads A's X and remembers A's Y and B's X and Y for STICK(1) to STICK(3) (per program, forgotten when it ends); positions are 1 to 200 ((value - JOY_AXIS_MIN) x 199 / 65535 + 1), 0 without that joystick. Even STRIG numbers use joyButtonWentDown (0 A button 1, 2 B button 1, 4 A button 2, 6 B button 2), odd ones the state joyRead reports; True is -1.

    Writing Games for DVX

    -

    A game in DVX is a program with its own main loop (hasMainLoop = true in its SHELL_APP_DESCRIPTOR). DVX gives it its own task: appMain runs the game loop itself, draws into the game's window, and waits for each frame on the high-resolution clock while the other programs keep running. Nothing in DVX has to stop for a game: the desktop, other programs, the Task Manager (Ctrl+Esc) and help all stay available.

    +

    A game in DVX is a program with its own main loop (hasMainLoop = true in its SHELL_APP_DESCRIPTOR). DVX gives it its own task: appMain runs the game loop itself, draws into the game's window, and waits for each frame on the high-resolution clock while the other programs keep running. Nothing in DVX has to stop for a game: the desktop, other programs, the Task Manager (Ctrl+Esc), and help all stay available.

    Starting From a Template

      bin/sdk/newProject.sh game myGame        a C game
       bin/sdk/newProject.sh basicgame myGame   a DVX BASIC game
    -

    The C template is a small but complete game, and every part of this guide is shown in it: a game input window, a 320x200 screen of 8-bit colour numbers drawn scaled into the window on any desktop colour depth, frame pacing, the arrow keys and a joystick, a sound effect and music carried inside the .app as resources, tones for the end of a round, the best score saved in the program's settings folder, its own help file (F1) and icon. The BASIC template does the same in a few dozen lines with FRAMEWAIT, GameKeyDown, STICK, STRIG and PLAY.

    +

    The C template is a small but complete game, and every part of this guide is shown in it: a game input window, a 320x200 screen of 8-bit colour numbers drawn scaled into the window on any desktop colour depth, frame pacing, the arrow keys and a joystick, a sound effect and music carried inside the .app as resources, tones for the end of a round, the best score saved in the program's settings folder, its own help file (F1) and icon. The BASIC template does the same in a few dozen lines with FRAMEWAIT, GameKeyDown, STICK, STRIG, and PLAY.

    The Shape of a Game

    int32_t appMain(DxeAppContextT *ctx) {
         GameFramePacerT pacer;
    @@ -8444,30 +8815,30 @@ free(matches);
    return 0; }

    The sections that follow cover each part.

    -

    Keyboard, Mouse and Joysticks

    +

    Keyboard, Mouse, and Joysticks

    Timing and Frame Pacing

    The Screen: Framebuffers and Palettes

    -

    Sound, Music and MIDI

    +

    Sound, Music, and MIDI

    Data Files and Saved Games

    Packaging and Ending

    Games in DVX BASIC

    Porting a Game Library to DVX

    -

    Keyboard, Mouse and Joysticks

    +

    Keyboard, Mouse, and Joysticks

    Game Input Windows

    Set gameInput on the game's window and give it an onRawKey callback if it wants every key going down and up:

    win->gameInput = true;
     win->onKey     = gameKey;       // typed keys: every key but three
     win->onRawKey  = gameRawKey;    // each key going down and up
    -

    While the window has the focus, DVX keeps only three keys, so the user can always switch away, open the Task Manager or close the game:

    +

    While the window has the focus, DVX keeps only three keys, so the user can always switch away, open the Task Manager, or close the game:

      Key         Always does
       ---         -----------
       Alt+Tab     Switch to the next window
       Ctrl+Esc    Open the Task Manager
       Alt+F4      Close the window (its onClose)
    -

    Every other key, F1, F10, Alt+letter and the accelerators included, goes to onKey instead of opening help or menus. The game decides what F1 does (the template opens its help with shellOpenHelp); its menus still work with the mouse. onRawKey gets a RawKeyEventT for every key going down or up: the set-1 scancode with an extended flag for the grey keys, down or up, whether it is a typematic repeat, and the modifiers. Shift, Ctrl and Alt are keys of their own there, left and right apart.

    -

    The key that completes one of the three (Esc with Ctrl held, Tab or F4 with Alt held) never reaches the game: not in onRawKey, not in the held-key table, not its repeats and not its release. The modifier itself does, because when Ctrl or Alt goes down nobody can tell yet what will follow. A game that treats any key as "go on" therefore still sees the Ctrl or Alt of Ctrl+Esc or Alt+Tab.

    +

    Every other key, F1, F10, Alt+letter, and the accelerators included, goes to onKey instead of opening help or menus. The game decides what F1 does (the template opens its help with shellOpenHelp); its menus still work with the mouse. onRawKey gets a RawKeyEventT for every key going down or up: the set-1 scancode with an extended flag for the grey keys, down or up, whether it is a typematic repeat, and the modifiers. Shift, Ctrl, and Alt are keys of their own there, left and right apart.

    +

    The key that completes one of the three (Esc with Ctrl held, Tab or F4 with Alt held) never reaches the game: not in onRawKey, not in the held-key table, not its repeats, and not its release. The modifier itself does, because when Ctrl or Alt goes down nobody can tell yet what will follow. A game that treats any key as "go on" therefore still sees the Ctrl or Alt of Ctrl+Esc or Alt+Tab.

    Held Keys

    For "is it held right now" use gameKeyIsDown(win, rawCode) with a raw key code (KEY_RAW_UP, KEY_RAW_LCTRL, or KEY_RAW_CODE(scan, extended) for any key); it is true only while win has the focus. platformKeyIsDown and platformKeyStateGet read the table itself. When the focus moves away, the game gets a release for every key still held and the table starts over, so no key stays stuck down. A key already held when the window gets the focus is seen once it repeats.

    Typed Characters and the Mouse

    @@ -8486,7 +8857,7 @@ win->onRawKey = gameRawKey; // each key going down and up

    The Screen: Framebuffers and Palettes

    -

    A window's content buffer (win->contentBuf, win->contentPitch bytes per row, contentW x contentH pixels) is in the desktop's own pixel format: 8, 15, 16 or 32 bits per pixel, as dvxGetDisplay(ctx)->format says. A game that must look the same everywhere draws into a framebuffer of its own (for example 320x200 bytes, each a colour number into a 256-colour game palette) and converts only what changed into the content buffer each frame.

    +

    A window's content buffer (win->contentBuf, win->contentPitch bytes per row, contentW x contentH pixels) is in the desktop's own pixel format: 8, 15, 16, or 32 bits per pixel, as dvxGetDisplay(ctx)->format says. A game that must look the same everywhere draws into a framebuffer of its own (for example 320x200 bytes, each a colour number into a 256-colour game palette) and converts only what changed into the content buffer each frame.

    The Colour Lookup Table

    Keep one desktop pixel value per game colour, and rebuild it when the game palette changes or the desktop's pixel format or palette does (the user can change the video mode while the game runs):

    const DisplayT *d = dvxGetDisplay(ctx);
    @@ -8503,7 +8874,7 @@ for (int32_t i = 0; i < 256; i++) {
       16-bit      2 bytes       5-6-5 bits of red, green, blue: very close.
       15-bit      2 bytes       5 bits each: very close.
       8-bit       1 byte        The nearest of the desktop's 256 colours.
    -

    On an 8-bit desktop all windows share the one desktop palette: a 6x6x6 colour cube, a 16-step grey ramp and the chrome colours (see dvxPal.h). A game cannot change it; gamePalNearest finds the closest entry, and a GamePalCacheT remembers the answer per 15-bit colour when there are many colours to map (gamePalCacheSync starts over if the desktop palette changed). Gradients come out banded; design for it, or ask players to use a 15-bit or better desktop for the best look.

    +

    On an 8-bit desktop all windows share the one desktop palette: a 6x6x6 colour cube, a 16-step grey ramp, and the chrome colours (see dvxPal.h). A game cannot change it; gamePalNearest finds the closest entry, and a GamePalCacheT remembers the answer per 15-bit colour when there are many colours to map (gamePalCacheSync starts over if the desktop palette changed). Gradients come out banded; design for it, or ask players to use a 15-bit or better desktop for the best look.

    Palette animation (colour cycling, fades) works at every depth: change the game palette entries, look up just those entries again, and redraw the pixels that use them. Games whose hardware picks a palette per scan line keep one small table per palette and pick the table per row.

    Presenting a Frame

      @@ -8533,31 +8904,31 @@ if (!dvxExclusivePresent8(ctx, fb, 320, 200, 320)) {
      • dvxExclusiveBegin sets the smallest mode of at least the size asked for, at the depth asked for when there is one (0: any). At 8 bits per pixel a request that fits 320x200 gets VGA mode 13h, and one that fits 320x240 gets VGA Mode X (square pixels, and two pages: a whole-screen flush is shown from the next retrace, so frames never tear), unless the card has a VBE mode no larger. Either way the game draws into an ordinary linear backbuffer. The display may be larger than asked for; dvxExclusiveDisplay says what it is.
      • The game's window must have the focus. When it loses it (Alt+Tab, Ctrl+Esc, a dialog, another program's window coming to the front) the desktop comes back and the callback hears ExclusiveSuspendedE; when the window has the focus again the mode is set again and it hears ExclusiveActiveE. Draw the whole frame then: the screen starts black.
      • -
      • While the game has the screen, every key but Alt+Tab, Ctrl+Esc and Alt+F4 goes to its window, as in a game input window; the mouse cursor is not drawn; onMouse gets whole-screen coordinates.
      • +
      • While the game has the screen, every key but Alt+Tab, Ctrl+Esc, and Alt+F4 goes to its window, as in a game input window; the mouse cursor is not drawn; onMouse gets whole-screen coordinates.
      • dvxExclusivePresent8 shows a picture of palette numbers on the whole screen, scaled by the largest whole factor that fits and centred on black, in any pixel format. At 8 bits per pixel the palette is the screen's own: dvxExclusiveSetPalette changes colours at once, so palette animation costs nothing, and a hardware model of up to 16 palettes of 16 colours fits in the 256 entries.
      • To draw yourself, draw into dvxExclusiveDisplay(ctx)->backBuf (DVX's draw functions work on it with dvxExclusiveBlitOps) and call dvxExclusiveFlush. dvxExclusiveWaitRetrace waits for the vertical retrace before a flush, so the frame is not torn.
      • -
      • dvxExclusiveEnd gives the screen back. Closing the window, the program ending and End Task do too.
      • +
      • dvxExclusiveEnd gives the screen back. Closing the window, the program ending, and End Task do too.
    -

    Sound, Music and MIDI

    +

    Sound, Music, and MIDI

    Games play sound on whatever the computer has: a Sound Blaster family card, an FM synthesizer chip, a MIDI port, the PC speaker, or nothing. Nothing fails because of missing hardware: audioOpen returns NULL (or the device plays tones only), and the game carries on silently. Several programs can play at once, and everything a program opened is stopped and released when it ends.

    Sound Effects and Tones (audio.h)

    • audioOpen() gives the program an audio context (the first one opened starts the sound device).
    • audioSampleLoad / audioSampleLoadMem load a WAV or VOC file (from memory: a resource, say); audioSampleCreate makes a sample from raw PCM (8-bit unsigned or 16-bit signed, mono or stereo, any rate from 4000 to 48000 Hz).
    • -
    • audioPlay(sample, volume, pan, loop) plays it on a voice; the voice id sets volume, pan and pitch while it plays (audioVoiceSet...), audioVoiceStop stops it, and audioSetEndFn tells when it ends.
    • -
    • audioTone plays a square, triangle or noise tone of a frequency and length; audioNotesOpen gives a queue of notes and rests played back to back, each to the exact sample (jingles, chip-style tunes).
    • +
    • audioPlay(sample, volume, pan, loop) plays it on a voice; the voice id sets volume, pan, and pitch while it plays (audioVoiceSet...), audioVoiceStop stops it, and audioSetEndFn tells when it ends.
    • +
    • audioTone plays a square, triangle, or noise tone of a frequency and length; audioNotesOpen gives a queue of notes and rests played back to back, each to the exact sample (jingles, chip-style tunes).
    • audioStreamOpen takes the game's own sound at any rate: written with audioStreamWrite, or asked for by a fill function.
    • audioSetVolume sets the program's volume; the master volume belongs to the user (Control Panel).

    Music (music.h)

    -

    musicLoad or musicLoadMem load a tracker module (MOD, S3M, XM, IT) or a Standard MIDI File; musicPlay(music, loop) plays it, with pause, resume, stop, volume, position and an end notification. MIDI plays on the best synthesizer there is: an external one on the MPU-401 port when the user chose it, else the FM chip with a General MIDI bank, else DVX's own FM software synthesizer; modules play on any card that plays sampled sound. musicMidiOpen gives live MIDI (note on and off, program change, controllers, pitch bend), and musicMmlPlay plays tunes written in QBasic's music macro language.

    +

    musicLoad or musicLoadMem load a tracker module (MOD, S3M, XM, IT) or a Standard MIDI File; musicPlay(music, loop) plays it, with pause, resume, stop, volume, position, and an end notification. MIDI plays on the best synthesizer there is: an external one on the MPU-401 port when the user chose it, else the FM chip with a General MIDI bank, else DVX's own FM software synthesizer; modules play on any card that plays sampled sound. musicMidiOpen gives live MIDI (note on and off, program change, controllers, pitch bend), and musicMmlPlay plays tunes written in QBasic's music macro language.

    The FM Chip and the MIDI Port

    For music drivers of its own, a game can allocate two-operator FM voices shared with the other programs (audioFmAlloc, audioFmWrite with the AUDIO_FM_* register numbers), take the whole FM chip for itself (audioFmRawAcquire; refused with AUDIO_ERR_BUSY while another program has it), or send bytes to the MIDI port (audioMidiAcquire, audioMidiPut). Give them back as soon as the game does not need them.

    Keeping Sound Fed

    -

    Sampled sound is mixed from the sound card's own interrupt, so it keeps playing however busy the game is. End notifications, pull streams, modules and the software synthesizer are made in the desktop's idle time; a game that redraws every frame should call audioPump() and musicPump() once a frame so they keep up. Modules use floating point: on a computer without a maths coprocessor they are slow.

    -

    See the Sound Library, the Music Library and the Joystick Library for every function.

    +

    Sampled sound is mixed from the sound card's own interrupt, so it keeps playing however busy the game is. End notifications, pull streams, modules, and the software synthesizer are made in the desktop's idle time; a game that redraws every frame should call audioPump() and musicPump() once a frame so they keep up. Modules use floating point: on a computer without a maths coprocessor they are slow.

    +

    See the Sound Library, the Music Library, and the Joystick Library for every function.

    Data Files and Saved Games

    @@ -8581,9 +8952,9 @@ if (!dvxExclusivePresent8(ctx, fb, 320, 200, 320)) {

    Packaging and Ending

    Packaging

    -

    make package lays out pkg/APPS/<VENDOR>/<NAME>/ with NAME.app, NAME.hlp and the files listed in DATA; zip the contents of pkg/ and the player unpacks it into the DVX directory (make install DVX_DIR=... does it for you). The .app carries its icon, name, author and description for Program Manager (NAME.res). Every game should have its own help (NAME.dhs, compiled to NAME.hlp), since F1 in a game input window goes to the game: open the help from the game's F1 and from Help > Contents. A game built on a static library of its own links it in with the LIBS variable (LIBS = port/libgame.a).

    +

    make package lays out pkg/APPS/<VENDOR>/<NAME>/ with NAME.app, NAME.hlp, and the files listed in DATA; zip the contents of pkg/ and the player unpacks it into the DVX directory (make install DVX_DIR=... does it for you). The .app carries its icon, name, author, and description for Program Manager (NAME.res). Every game should have its own help (NAME.dhs, compiled to NAME.hlp), since F1 in a game input window goes to the game: open the help from the game's F1 and from Help > Contents. A game built on a static library of its own links it in with the LIBS variable (LIBS = port/libgame.a).

    Ending

    -

    Close the game's window and return from appMain. Every sound, music, joystick handle and timer the program opened is released when it ends, however it ends, so a crash or End Task leaves nothing playing. A game (or any program) that calls exit() or abort() ends only itself, like End Task: its windows close, its timers are removed and DVX keeps running. Handlers registered with atexit are not run; clean up before calling exit(), or return from appMain. malloc and free work as usual; DVX counts each program's memory (the Task Manager shows it).

    +

    Close the game's window and return from appMain. Every sound, music, joystick handle, and timer the program opened is released when it ends, however it ends, so a crash or End Task leaves nothing playing. A game (or any program) that calls exit() or abort() ends only itself, like End Task: its windows close, its timers are removed, and DVX keeps running. Handlers registered with atexit are not run; clean up before calling exit(), or return from appMain. malloc and free work as usual; DVX counts each program's memory (the Task Manager shows it).

    Games in DVX BASIC

    @@ -8596,13 +8967,13 @@ if (!dvxExclusivePresent8(ctx, fb, 320, 200, 320)) { FRAMEWAIT 30 Loop Until quitting ' set in the form's Unload event End Sub
    -

    GameKeyDown (game.bas) tells whether a key is held, GameKeyClear forgets the held keys, FrameMissed() counts the frames FRAMEWAIT skipped and GameWaitRetrace waits for the screen's retrace; QBasic's STICK and STRIG read joysticks (the keyboard joystick when there is none); BEEP, SOUND and PLAY make sound, PLAY "MB ..." in the background so the game does not wait. sound.bas plays WAV files and tones, sounds loaded once and played with volume, pan and pitch, sound the game makes as it goes (streams) and FM instruments; music.bas plays modules and MIDI files, and joystick.bas reads joysticks in full. See Sound, Joysticks and Frames and Include Libraries in the BASIC reference.

    +

    GameKeyDown (game.bas) tells whether a key is held, GameKeyClear forgets the held keys, FrameMissed() counts the frames FRAMEWAIT skipped, and GameWaitRetrace waits for the screen's retrace; QBasic's STICK and STRIG read joysticks (the keyboard joystick when there is none); BEEP, SOUND, and PLAY make sound, PLAY "MB ..." in the background so the game does not wait. sound.bas plays WAV files and tones, sounds loaded once and played with volume, pan, and pitch, sound the game makes as it goes (streams), and FM instruments; music.bas plays modules and MIDI files, and joystick.bas reads joysticks in full. See Sound, Joysticks, and Frames and Include Libraries in the BASIC reference.

    Porting a Game Library to DVX

    Portable game libraries for DOS-era machines usually keep everything machine-specific behind a small hardware layer: start and stop, show a frame, set the palette, read input, wait for the next frame, play sound, and find files. A DVX port replaces that layer. The rest of the library, including drawing code, sprite compilers that write x86 code into memory, and asset formats built for DOS, works unchanged, because a DVX program is 32-bit protected-mode x86 code in the same flat memory as a DOS program built with the same compiler.

    The Model: a Window, a Task, a Virtual Frame Rate

    -

    The game becomes a DVX program with its own main loop: appMain calls the game's main() (compile the game with -Dmain=gameMain, say, and call gameMain from appMain). The library's start-up creates a game input window whose content area is the game's screen at a whole scale; its frame wait yields until the next frame on the high-resolution clock. In a window the game shares the screen with the desktop, so there is no mode change, no direct palette hardware and no vertical retrace; the port converts the library's back buffer into the window each frame. On the whole screen (see The Whole Screen) the game has its own mode, its own palette and the vertical retrace, while the other programs run on.

    +

    The game becomes a DVX program with its own main loop: appMain calls the game's main() (compile the game with -Dmain=gameMain, say, and call gameMain from appMain). The library's start-up creates a game input window whose content area is the game's screen at a whole scale; its frame wait yields until the next frame on the high-resolution clock. In a window the game shares the screen with the desktop, so there is no mode change, no direct palette hardware, and no vertical retrace; the port converts the library's back buffer into the window each frame. On the whole screen (see The Whole Screen) the game has its own mode, its own palette, and the vertical retrace, while the other programs run on.

    Mapping a Hardware Layer to DVX

      Hardware layer                   DVX
       --------------                   ---
    @@ -8661,7 +9032,7 @@ End Sub
  • The 256-colour desktop: nearest colours only; say so in the game's help if it needs 15-bit colour or better.
  • Checking a Port

    -

    The library's own drawing tests (hashes of the back buffer after each drawing call) should give the same answers as on DOS, since the drawing code is the same. Test in a 256-colour and a 16-bit desktop, with and without a sound card and a joystick, and with other programs running; End Task and exit() must leave DVX, the sound and the timer as they were.

    +

    The library's own drawing tests (hashes of the back buffer after each drawing call) should give the same answers as on DOS, since the drawing code is the same. Test in a 256-colour and a 16-bit desktop, with and without a sound card and a joystick, and with other programs running; End Task and exit() must leave DVX, the sound, and the timer as they were.

    SDK Quick Start

    @@ -8670,13 +9041,13 @@ End Sub ---- -------- newProject.sh Creates a project from a template sdk.mk Build rules every project Makefile includes - bin/ dvxres (resources), dvxhlpc (help compiler), bascomp (BASIC compiler), dxeGen.sh (runs dxe3gen) + bin/ dvxres (resources), dvxhlpc (help compiler), bascomp (BASIC compiler), mkfont (fonts a program carries), dxeGen.sh (runs dxe3gen) include/ Public C headers, one directory per library, plus include/basic for BASIC declaration files templates/ The project templates samples/basic/ Example BASIC projects

    Requirements

      -
    • A Linux host (the tools in bin/ are Linux programs; on DVX itself they are SYSTEM/DVXRES.EXE, SYSTEM/DVXHLPC.EXE and SYSTEM/BASCOMP.EXE).
    • +
    • A Linux host (the tools in bin/ are Linux programs; on DVX itself they are SYSTEM/DVXRES.EXE, SYSTEM/DVXHLPC.EXE, and SYSTEM/BASCOMP.EXE).
    • The DJGPP cross compiler (i586-pc-msdosdjgpp-gcc and dxe3gen). Projects find it through DJGPP_PREFIX, from the environment or the make command line. Without it they use toolchains/djgpp of the DVX source tree the SDK sits in (SDK_DIR/../../toolchains/djgpp), which ./installToolchain.sh sets up.

    Creating a Project

    @@ -8688,13 +9059,13 @@ End Sub library A C library of shared functions (NAME.lib) with BASIC declarations basic A DVX BASIC program (NAME.dbp, built to NAME.app) game A C game (NAME.app): a 320x200 screen in a window, frame pacing, keyboard and joystick, sound effects and music - basicgame A DVX BASIC game (NAME.dbp): a FRAMEWAIT loop, GameKeyDown, STICK, STRIG and PLAY + basicgame A DVX BASIC game (NAME.dbp): a FRAMEWAIT loop, GameKeyDown, STICK, STRIG, and PLAY control A control written in DVX BASIC (NAME.dbp with NAME.ctl, built to NAME.wgt) usable from C programs and DVX BASIC

    The two game templates are explained in Writing Games for DVX.

    -

    NAME is a letter followed by letters and digits, 8 characters at most, because it becomes a DOS file name. Files are named NAME with its first letter in lower case (camelCase, such as myApp); DVX shows it in upper case. The script refuses DOS device names (CON, PRN, AUX, NUL, COM1 and so on), the name of a widget DVX already has, and an existing directory. The project goes in DIR, or a new directory of that name in the current one.

    -

    The template's names become yours everywhere: file names, identifiers in each case style (tplName, TplName, tplname, TPLNAME), help topic ids and the ABI stamp. The Makefile refers to the SDK by a relative path, so a project and the SDK can move together.

    +

    NAME is a letter followed by letters and digits, 8 characters at most, because it becomes a DOS file name. Files are named NAME with its first letter in lower case (camelCase, such as myApp); DVX shows it in upper case. The script refuses DOS device names (CON, PRN, AUX, NUL, COM1, and so on), the name of a widget DVX already has, and an existing directory. The project goes in DIR, or a new directory of that name in the current one.

    +

    The template's names become yours everywhere: file names, identifiers in each case style (tplName, TplName, tplname, TPLNAME), help topic ids, and the ABI stamp. The Makefile refers to the SDK by a relative path, so a project and the SDK can move together.

    Building

    -

    Every project Makefile only names the SDK, the kind and the module; the rules come from sdk.mk:

    +

    Every project Makefile only names the SDK, the kind, and the module; the rules come from sdk.mk:

      Command                            Does
       -------                            ----
       make                               Builds the module with its resources, and NAME.hlp from the help source
    @@ -8703,7 +9074,7 @@ End Sub
    make clean Removes everything the build made

    Every .c file in the project directory is compiled with warnings as errors (-Wall -Wextra -Werror) and every SDK include directory on the path. Add your own flags with CFLAGS, for example CFLAGS=-I../myLib to use a library's header. VENDOR (default local) is the vendor directory the module installs under.

    Programs That Use the Network

    -

    The dvxnet library provides TCP/IP through the standard socket calls (socket, connect, send, recv, getaddrinfo, select and so on). Its headers are in include/net: dvxNet.h, plus sys/socket.h, netinet/in.h, netinet/tcp.h, arpa/inet.h, netdb.h, poll.h, sys/select.h, sys/ioctl.h and an errno.h that adds the socket error codes to the C library's. Because they stand in for headers of the same name, include/net is not on every module's path; a module that uses sockets adds one line to its Makefile, before the include of sdk.mk:

    +

    The dvxnet library provides TCP/IP through the standard socket calls (socket, connect, send, recv, getaddrinfo, select, and so on). Its headers are in include/net: dvxNet.h, plus sys/socket.h, netinet/in.h, netinet/tcp.h, arpa/inet.h, netdb.h, poll.h, sys/select.h, sys/ioctl.h, and an errno.h that adds the socket error codes to the C library's. Because they stand in for headers of the same name, include/net is not on every module's path; a module that uses sockets adds one line to its Makefile, before the include of sdk.mk:

      NET = 1

    sdk.mk then puts include/net ahead of the C library's headers, and the program uses the ordinary names:

      #include <sys/socket.h>
    @@ -8742,14 +9113,15 @@ End Sub
      File         Purpose
       ----         -------
       hello.c      The program
    -  hello.res    Resources appended to hello.app: its icon, name, author, description and a greeting text
    +  hello.res    Resources appended to hello.app: its icon, name, author, description, and a greeting text
       icon32.bmp   The 32x32 icon Program Manager shows
       hello.dhs    The program's help, compiled to hello.hlp
    -  Makefile     Names the SDK, KIND = app and NAME = hello
    + Makefile Names the SDK, KIND = app, and NAME = hello

    How the Program Works

    -

    SHELL_APP_DESCRIPTOR(...) declares the exported appDescriptor: the program's name, whether it has its own main loop, whether several copies may run, its task stack size and priority. It also records the binary interface version the program was built for.

    -

    DVX calls appMain(DxeAppContextT *ctx) once. The template is a callback-only program (hasMainLoop = false): appMain creates a window with a File and Help menu, a label and a button, and returns 0. From then on DVX calls the window's onClose and onMenu callbacks and the button's onClick. Destroying the program's last window ends it. A program that needs to keep working between events sets hasMainLoop = true, loops in appMain and calls tsYield() often (see the Clock program's source for an example).

    -

    ctx gives the program its paths: appPath (the .app file, used to read its own resources with dvxResOpen and dvxResRead), appDir, configDir (a writable directory for its settings) and helpFile (preset to hello.hlp beside hello.app).

    +

    SHELL_APP_DESCRIPTOR(...) declares the exported appDescriptor: the program's name, whether it has its own main loop, whether several copies may run, its task stack size, and priority. It also records the binary interface version the program was built for.

    +

    DVX calls appMain(DxeAppContextT *ctx) once. The template is a callback-only program (hasMainLoop = false): appMain creates a window with a File and Help menu, a label, and a button, and returns 0. From then on DVX calls the window's onClose and onMenu callbacks and the button's onClick. Destroying the program's last window ends it. A program that needs to keep working between events sets hasMainLoop = true, loops in appMain, and calls tsYield() often (see the Clock program's source for an example).

    +

    ctx gives the program its paths: appPath (the .app file, used to read its own resources with dvxResOpen and dvxResRead), appDir, configDir (a writable directory for its settings), and helpFile (preset to hello.hlp beside hello.app).

    +

    A program can carry fonts of its own as font resources, so its text looks the same whatever fonts were installed: add a line such as "title font TITLE.DFN" to hello.res (dvxres checks that the file is a DVX font). DVX adds the program's fonts when it starts and forgets them when it ends; other programs never see them, and where a family is installed too, the program's sizes and styles come first (dvxText.h, Fonts a Program Carries). Make a .DFN with bin/mkfont from a TrueType, Type 1, or bitmap font -- its manifest names the sizes, whether they are smoothed too, and the characters to keep (mkfont.c's header documents every key; one size and only the characters the program shows keep the file small) -- or copy one from DVX's FONTS folder. A .DFN carries the notice and licence of the font it was made from.

    Run

      make install DVX_DIR=/path/to/dvx

    This copies hello.app and hello.hlp to APPS/LOCAL/HELLO/ in the DVX directory. Start DVX and run the program from Program Manager (File > Run), or add an icon for it. After a change, rebuild and install again; close the running copy first.

    @@ -8770,7 +9142,7 @@ End Sub
      File         Purpose
       ----         -------
       badge.c      The widget: class, API table, BASIC interface, wgtRegister
    -  badge.h      The public API: BadgeApiT, the dvxBadgeApi() accessor and the wgtBadge... macros
    +  badge.h      The public API: BadgeApiT, the dvxBadgeApi() accessor, and the wgtBadge... macros
       badge.res    Resources: icon24 (the DVX BASIC toolbox icon) and name (its tool tip)
       icon24.bmp   The 24x24 toolbox icon
       badge.dhs    Developer help, added to the system help
    @@ -8781,14 +9153,14 @@ End Sub
    • A widget class (WidgetClassT): the paint and minimum-size handlers, and the shared text handlers (the widget's data starts with its text, so widgetAllocWithText creates it and WCLASS_HAS_TEXT frees it).
    • An API table (BadgeApiT) under the name "badge". It starts with WgtApiHeaderT, filled by WGT_API_HEADER. Programs reach it through the macros in badge.h, which call wgtGetApi("badge", sizeof(BadgeApiT)).
    • -
    • A BASIC interface (WgtIfaceT): the control name Badge, a Style property, a Clear method and Click as the default event. The form runtime adds the common properties (Caption, Left, Top, Width, Height, Visible, Enabled...) and events itself. wgtIfaceSetType binds the interface to the class, so wgtGetIfaceOf and the generic calls (wgtGetProp, wgtSetProp, wgtCallMethod) find it from any badge widget.
    • +
    • A BASIC interface (WgtIfaceT): the control name Badge, a Style property, a Clear method, and Click as the default event. The form runtime adds the common properties (Caption, Left, Top, Width, Height, Visible, Enabled...) and events itself. wgtIfaceSetType binds the interface to the class, so wgtGetIfaceOf and the generic calls (wgtGetProp, wgtSetProp, wgtCallMethod) find it from any badge widget.

    DVX_ABI_STAMP(badge) at file scope records the binary interface version. Its argument must be the module's file name in lower case.

    Run

    Install it (make install DVX_DIR=/path/to/dvx puts badge.wgt and its help in WIDGETS/LOCAL/BADGE/) and restart DVX. The widget is then:

    • In the DVX BASIC toolbox: draw a Badge on a form, set its Style in the property panel.
    • -
    • Usable from a C program: add CFLAGS=-I../badge to the program's make command (or Makefile), include badge.h and call wgtBadge(parent, "Text"). wgtGetApi returns NULL when the widget is not installed, so a program that can run without it checks dvxBadgeApi() before using the macros.
    • +
    • Usable from a C program: add CFLAGS=-I../badge to the program's make command (or Makefile), include badge.h, and call wgtBadge(parent, "Text"). wgtGetApi returns NULL when the widget is not installed, so a program that can run without it checks dvxBadgeApi() before using the macros.
    Warning: DVX stops at start-up with a message naming a widget or library it cannot load or that was built for a different version. Remove that module's directory to start DVX again.

    Help

    @@ -8817,13 +9189,13 @@ End Sub mathx.dhs Developer help, added to the system help Makefile KIND = library

    How the Library Works

    -

    DVX loads every library when it starts, before any widget or program, in the order the .dep files require. Every function that is not static is exported: a program, widget or library loaded later calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file.

    +

    DVX loads every library when it starts, before any widget or program, in the order the .dep files require. Every function that is not static is exported: a program, widget, or library loaded later calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file.

    BASIC programs add mathx.bas to their project. DECLARE LIBRARY looks up each function by exactly the name it declares, without a trailing $ (a $ marks a function returning a string), so mathxGreet$ calls the C function mathxGreet. Keep the declarations and the C functions in step; strings are passed as const char * and returned as const char *.

    -

    A string declared AS STRING is text and stops at the first zero byte. For bytes (network data, samples, encrypted data) declare BINARY STRING: each BINARY STRING parameter becomes two C parameters at its place, the bytes (const uint8_t *, a private copy valid during the call, followed by a zero byte) and their count (int32_t); a BINARY STRING result adds one last C parameter, int32_t *outLen, where the function stores the length of the bytes it returns (the runtime copies them right after the call, so a static buffer is fine; NULL, a negative length or a length never set give an empty string). The template's tplNameXor shows both:

    +

    A string declared AS STRING is text and stops at the first zero byte. For bytes (network data, samples, encrypted data) declare BINARY STRING: each BINARY STRING parameter becomes two C parameters at its place, the bytes (const uint8_t *, a private copy valid during the call, followed by a zero byte) and their count (int32_t); a BINARY STRING result adds one last C parameter, int32_t *outLen, where the function stores the length of the bytes it returns (the runtime copies them right after the call, so a static buffer is fine; NULL, a negative length, or a length never set give an empty string). The template's tplNameXor shows both:

      // tplNameXor$(BYVAL bytes AS BINARY STRING, BYVAL key AS LONG) AS BINARY STRING
       const uint8_t *tplNameXor(const uint8_t *data, int32_t len, int32_t key, int32_t *outLen);

    Run

    -

    Install it (LIBS/LOCAL/MATHX/ gets mathx.lib, mathx.dep and MATHX.DHS) and restart DVX. Build programs that use it with CFLAGS=-I../mathx.

    +

    Install it (LIBS/LOCAL/MATHX/ gets mathx.lib, mathx.dep, and MATHX.DHS) and restart DVX. Build programs that use it with CFLAGS=-I../mathx.

    Help

    mathx.dhs goes into the system help, under Technical Reference > Libraries, when DVX next starts with the library installed.

    Next

    @@ -8845,9 +9217,9 @@ End Sub icon32.bmp The program's icon (the project's Icon entry) greeter.dhs The program's help, compiled to greeter.hlp Makefile KIND = basic -

    The project lists the SDK's include/basic/help.bas, which declares HelpView; Help > Contents calls HelpView "greeter.hlp", which opens the file beside the program. The project settings in the .dbp take the place of a C program's resource file.

    +

    The project lists the SDK's include/basic/help.bas, which declares HelpView; Help > Contents calls HelpView "greeter.hlp", which opens the file beside the program. The project settings in the .dbp take the place of a C program's resource file; fonts the program carries go in its [Fonts] section (Project > Properties in DVX BASIC), and the build makes them font resources.

    Run and Debug

    -

    make install puts greeter.app and greeter.hlp in APPS/LOCAL/GREETER/. You can also copy the project directory to the DVX machine and open greeter.dbp in DVX BASIC (File > Open Project...): Run > Run (F5) runs it, Run > Debug sets it going under the debugger with breakpoints (F9), stepping and the locals, watch and call stack windows, and File > Make Executable... builds the .app there. The DVX BASIC help describes the language and the IDE.

    +

    make install puts greeter.app and greeter.hlp in APPS/LOCAL/GREETER/. You can also copy the project directory to the DVX machine and open greeter.dbp in DVX BASIC (File > Open Project...): Run > Run (F5) runs it, Run > Debug sets it going under the debugger with breakpoints (F9), stepping, and the locals, watch, and call stack windows, and File > Make Executable... builds the .app there. The DVX BASIC help describes the language and the IDE.

    Next

    Packaging and Installing

    @@ -8865,7 +9237,7 @@ End Sub icon24.bmp The toolbox picture (the header's ToolboxIcon) dial.bhs The control's BASIC help, installed beside it and added to the DVX BASIC help Makefile KIND = control -

    The control's Public properties, methods and events are what programs use; UserControl is its drawing surface. The DVX BASIC help (UserControls: Your Own Controls) describes the language side.

    +

    The control's Public properties, methods, and events are what programs use; UserControl is its drawing surface. The DVX BASIC help (UserControls: Your Own Controls) describes the language side.

    Install

    make install puts dial.wgt and DIAL.BHS in WIDGETS/LOCAL/DIAL/. When DVX starts it reads the control file through the BASIC run-time library and registers its controls; BASIC programs put a Dial on a form (Begin Dial Dial1 in a .frm, or from the designer's toolbox) and C programs create one by name (see Using BASIC Controls from C).

    Next

    @@ -8874,7 +9246,7 @@ End Sub

    Using BASIC Controls from C

    -

    A control written in DVX BASIC is an ordinary widget type once it is registered: it has a widget class, an interface (WgtIfaceT) with its properties, methods and events, and a type name equal to its BASIC name. A C program uses it through the generic widget access, without a header of its own:

    +

    A control written in DVX BASIC is an ordinary widget type once it is registered: it has a widget class, an interface (WgtIfaceT) with its properties, methods, and events, and a type name equal to its BASIC name. A C program uses it through the generic widget access, without a header of its own:

      WidgetT  *dial = wgtCreateByName("Dial", parent);
       WgtValueT v    = wgtValueInt(40);
       WgtValueT ret;
    @@ -8889,7 +9261,7 @@ End Sub
    ... // args[0]: the event's first argument } } -

    Property values follow the BASIC types: Integer, Long and Byte are WGT_IFACE_INT, Boolean WGT_IFACE_BOOL, Single WGT_IFACE_FLOAT, Double and Currency WGT_IFACE_DOUBLE, String, Date and Variant WGT_IFACE_STRING. A property declared As an Enum whose members are numbered 0, 1, 2 ... (each number once) is WGT_IFACE_ENUM with the member names in value order (WgtPropDescT.enumNames; wgtSetProp takes the number or a member name, wgtIfaceGetPropText gives the name); an Enum with other values is WGT_IFACE_INT. A method takes its arguments as they come (WGT_SIG_DYNAMIC) and returns its Function's value, or no value (WGT_IFACE_NONE) for a Sub. An event's numeric and Boolean arguments that onEvent changes go back to the control (ByRef parameters). Events the control names Click, DblClick, Change, KeyPress, KeyDown, KeyUp, MouseDown, MouseUp, MouseMove or Scroll go to the matching universal callback (onClick ...) instead; the dispatcher calls those for a BASIC control only when its code raises them (WCLASS_RAISES_EVENTS). The interface's events list each event with its parameters as BASIC writes them (WgtEventDescT.params).

    +

    Property values follow the BASIC types: Integer, Long, and Byte are WGT_IFACE_INT, Boolean WGT_IFACE_BOOL, Single WGT_IFACE_FLOAT, Double and Currency WGT_IFACE_DOUBLE, String, Date, and Variant WGT_IFACE_STRING. A property declared As an Enum whose members are numbered 0, 1, 2 ... (each number once) is WGT_IFACE_ENUM with the member names in value order (WgtPropDescT.enumNames; wgtSetProp takes the number or a member name, wgtIfaceGetPropText gives the name); an Enum with other values is WGT_IFACE_INT. A method takes its arguments as they come (WGT_SIG_DYNAMIC) and returns its Function's value, or no value (WGT_IFACE_NONE) for a Sub. An event's numeric and Boolean arguments that onEvent changes go back to the control (ByRef parameters). Events the control names Click, DblClick, Change, KeyPress, KeyDown, KeyUp, MouseDown, MouseUp, MouseMove, or Scroll go to the matching universal callback (onClick ...) instead; the dispatcher calls those for a BASIC control only when its code raises them (WCLASS_RAISES_EVENTS). The interface's events list each event with its parameters as BASIC writes them (WgtEventDescT.params).

    wgtCreateByName goes through the interface's dynamic create hook (WgtIfaceT.dyn); the class has no C API structure (wgtGetApi finds none), because one create function cannot tell which BASIC class to make.

    The Control File

    A control project builds NAME.wgt, a file that holds only resources:

    @@ -8902,7 +9274,7 @@ End Sub author, version ... The project's metadata; helpfile its help file's name

    The loader tells such a file from a module by its first bytes: instead of loading it, it hands it, once every library and native widget is loaded, to the BASIC run-time library's basCtlLoadFile (found with dlsym; without the library the file is skipped and logged). basCtlLoadFile registers each control in the file for every program and the loader records the file as theirs, so the toolbox finds their icon24 and name.

    The Bridge

    -

    Each control is one object of its BASIC class plus a border-less canvas, the UserControl surface its code draws on (the same drawing code as a BASIC PictureBox). The widget class forwards to the class's code: layout sizes the canvas, and between frames a new size runs UserControl_Resize and UserControl_Paint; the mouse, key, wheel and focus class methods run UserControl_MouseDown and the other handlers. A control file's code runs in one VM shared by every program that uses its controls; it never yields, and a call from another task while it runs code is refused (the paint keeps the old picture, the event is dropped). A BASIC program's own controls ([Controls] in its project) are registered by the program when it starts, owned by it, and run in its own VM.

    +

    Each control is one object of its BASIC class plus a border-less canvas, the UserControl surface its code draws on (the same drawing code as a BASIC PictureBox). The widget class forwards to the class's code: layout sizes the canvas, and between frames a new size runs UserControl_Resize and UserControl_Paint; the mouse, key, wheel, and focus class methods run UserControl_MouseDown and the other handlers. A control file's code runs in one VM shared by every program that uses its controls; it never yields, and a call from another task while it runs code is refused (the paint keeps the old picture, the event is dropped). A BASIC program's own controls ([Controls] in its project) are registered by the program when it starts, owned by it, and run in its own VM.

    Your First BASIC Control

    Your First Widget

    @@ -9095,7 +9467,7 @@ End Sub

    List Helper Library

    -

    Shared helper routines for dropdown and list-based widget DXEs (ListBox, Dropdown, ComboBox, ListView). Provides the owned-string item list, the single/multi-select model, drag-reorder helpers, the dropdown popup (state, geometry, painting, hit testing and keyboard handling), dropdown arrow rendering, item measurement, keyboard navigation, type-ahead search, popup scrollbar hit testing (with the same thumb geometry the window manager paints), and the icon view rules of Program Manager's view, which BASIC icon views share through listhelp.bas.

    +

    Shared helper routines for dropdown and list-based widget DXEs (ListBox, Dropdown, ComboBox, ListView). Provides the owned-string item list, the single/multi-select model, drag-reorder helpers, the dropdown popup (state, geometry, painting, hit testing, and keyboard handling), dropdown arrow rendering, text measurement in any font (fixed-pitch or proportional), keyboard navigation, type-ahead search, popup scrollbar hit testing (with the same thumb geometry the window manager paints), and the icon view rules of Program Manager's view, which BASIC icon views share through listhelp.bas.

    Header: listhelp/listHelp.h

    Loaded as: bin/libs/listhelp.lib

    Constants

    @@ -9110,23 +9482,25 @@ End Sub POPUP_SCROLLBAR_W SCROLLBAR_WIDTH Popup vertical scrollbar width (mirrors the core window-manager scrollbar width). LIST_REORDER_LINE_H 2 Thickness of the drag-reorder insertion line.

    ListItemsT (owned-string item list)

    -

    Item list shared by ListBox, Dropdown and ComboBox. items points either at a caller-owned array (isOwned false) or at owned, an stb_ds array of strdup'd strings (isOwned true). maxLen caches the widest item in characters and is maintained incrementally. NULL entries in a caller's array read as empty strings.

    +

    Item list shared by ListBox, Dropdown, and ComboBox. items points either at a caller-owned array (isOwned false) or at owned, an stb_ds array of strdup'd strings (isOwned true). maxW caches the widest item's width in pixels of widthFont (NULL until it is first measured); listItemsMaxWidth returns it, measuring every item again when asked for another font, and the mutators keep it current incrementally. NULL entries in a caller's array read as empty strings.

    typedef struct {
    -    const char **items;
    -    char       **owned;
    -    int32_t      count;
    -    int32_t      maxLen;
    -    bool         isOwned;
    +    const char        **items;
    +    char              **owned;
    +    const BitmapFontT  *widthFont;
    +    int32_t             count;
    +    int32_t             maxW;
    +    bool                isOwned;
     } ListItemsT;
     
    -bool        listItemsAdd(ListItemsT *li, const char *text);
    +bool        listItemsAdd(ListItemsT *li, const BitmapFontT *font, const char *text);
     void        listItemsAdopt(ListItemsT *li);
     bool        listItemsClear(ListItemsT *li);
     void        listItemsFree(ListItemsT *li);
     const char *listItemsGet(const ListItemsT *li, int32_t idx);
    -bool        listItemsRemove(ListItemsT *li, int32_t idx);
    -bool        listItemsSetExternal(ListItemsT *li, const char **items, int32_t count);
    -

    listItemsAdd and listItemsRemove first adopt (deep-copy) a caller's array, so they extend or shorten it without modifying it. listItemsSetExternal keeps the previously owned strings until the next adopt, clear or free, because the new array may point into them. The mutators return true when maxLen changed, i.e. when the widget's minimum width changed and it needs a relayout rather than just a repaint. listItemsGet returns "" for an index out of range.

    +int32_t listItemsMaxWidth(ListItemsT *li, const BitmapFontT *font); +bool listItemsRemove(ListItemsT *li, const BitmapFontT *font, int32_t idx); +bool listItemsSetExternal(ListItemsT *li, const BitmapFontT *font, const char **items, int32_t count); +

    listItemsAdd and listItemsRemove first adopt (deep-copy) a caller's array, so they extend or shorten it without modifying it. listItemsSetExternal keeps the previously owned strings until the next adopt, clear, or free, because the new array may point into them, and never reads the caller's previous array (it may be freed already). The mutators measure in font, the widget's own (wgtFont), and return true when the widest item's width changed, i.e. when the widget's minimum width changed and it needs a relayout rather than just a repaint. listItemsGet returns "" for an index out of range.

    widgetAdoptOwnedStrings(owned, src, count) builds the deep copy that adoption uses (complete before the old strings are freed, so src may alias them) and widgetFreeOwnedStrings(owned) frees every string of an owned array and empties it. ListView uses both for its row-major cell copy.

    ListSelT (selection model)

    Selection state shared by ListBox and ListView. cursor is the current (keyboard) item and anchor the start of Shift ranges; bits holds one byte per item in multi-select mode. Every index is a data index; functions that take a display index also take an optional display-to-data order map (NULL = identity) so a sorted view selects what the user sees as a contiguous range.

    @@ -9143,7 +9517,7 @@ bool listItemsSetExternal(ListItemsT *li, const char **items, int32_t cou listSelInsert(s, newCount, idx) An item was inserted at idx: existing selections are kept, cursor/anchor shift. listSelRemove(s, oldCount, idx) The item at idx was removed: bits close the gap, cursor/anchor shift; a removed cursor/anchor becomes -1. listSelReorder(s, from, dest) Drag-reorder moved an item: its bit moves with it and it becomes the cursor. - listSelClick(s, count, order, disp, shift, ctrl) Mouse selection: plain, Ctrl (toggle) or Shift (range from the anchor). + listSelClick(s, count, order, disp, shift, ctrl) Mouse selection: plain, Ctrl (toggle), or Shift (range from the anchor). listSelMove(s, count, order, disp, shift) Keyboard cursor move; Shift selects the range from the anchor. listSelKey(s, count, key, mod) Multi-select Ctrl+A (ListSelKeyAllE) and Space (ListSelKeyToggleE); ListSelKeyNoneE otherwise. listSelSetCursor(s, count, idx) Programmatic selection: idx becomes the only selected item. @@ -9160,7 +9534,7 @@ void listReorderTrack(int32_t relY, int32_t rowH, int32_t visibleRows, int32_ void listReorderPaint(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t top, int32_t w, int32_t h, int32_t rowH, int32_t scrollPos, int32_t dropIdx, uint32_t color);

    listReorderCancel abandons an item drag when the list changes under it (an item is removed or the items are replaced): it resets both indices and, when w is dragging an item, ends the drag so the rest of the mouse motion is ignored. listReorderDest maps a drag from item from to insertion point to (0..count) to the item's final index, or -1 when the drop is a no-op. listReorderMove moves one element of any size in place (a ListView row is colCount pointers). listReorderTrack updates the drop index from the pointer position and auto-scrolls one row when the pointer is on the first or last visible row. listReorderPaint draws the insertion line.

    ListPopupT (dropdown popup)

    -

    Popup state shared by Dropdown and ComboBox: the item list plus the selected item, the hovered item, the popup scroll position and whether it is open.

    +

    Popup state shared by Dropdown and ComboBox: the item list plus the selected item, the hovered item, the popup scroll position, and whether it is open.

    typedef struct {
         ListItemsT items;
         int32_t    selectedIdx;
    @@ -9173,15 +9547,15 @@ void    listReorderPaint(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t to
       listPopupInit(p)                                  No selection, no hover, closed.
       listPopupAddItem / listPopupRemoveItem            Item list edits; the selection and hover stay on the same item (-1 when it is removed).
       listPopupClearItems / listPopupSetItems           Replace the items; an out-of-range selection becomes -1.
    -  listPopupOpen(w, p)                               Open with the selection hovered and scrolled into view (keyboard, mouse and accelerator all use it).
    +  listPopupOpen(w, p)                               Open with the selection hovered and scrolled into view (keyboard, mouse, and accelerator all use it).
       listPopupClose(w, p)                              WGT_METHOD_CLOSE_POPUP body.
       listPopupCommit(p)                                Close and make the hovered item the selection; false when nothing valid is hovered.
       listPopupNavKey(p, key)                           Up/Down move the hover item, a printable key jumps to the next matching item.
    -  listPopupOnMouse(w, p, vx, vy)                    Click while open: ListPopupHitScrollE (scrollbar), ListPopupHitItemE (item selected, closed) or ListPopupHitNoneE (closed).
    +  listPopupOnMouse(w, p, vx, vy)                    Click while open: ListPopupHitScrollE (scrollbar), ListPopupHitItemE (item selected, closed), or ListPopupHitNoneE (closed).
       listPopupItemAt(p, font, popY, vy)                Item under vy, or -1 on the border or past the last item.
       listPopupPaint(w, p, ...)                         Overlay paint; uses the window content height, the same source as the hit test.
       listPopupDrawFrame(d, ops, colors, w, bg, down)   Sunken text area plus the drop button and arrow.
    -  listPopupCalcMinSize(w, font, p)                  Minimum size from the widest item.
    +  listPopupCalcMinSize(w, font, p)                  Minimum size from the widest item in font.
       listPopupEnsureVisible(p)                         Scroll the hover item into view.
       listPopupVisibleCount(itemCount)                  Rows the popup shows (1..DROPDOWN_MAX_VISIBLE).

    widgetDrawDropdownArrow

    @@ -9194,14 +9568,18 @@ void listReorderPaint(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t to centerX Horizontal center of the arrow in backbuffer coordinates. centerY Vertical center of the arrow in backbuffer coordinates. color Pre-packed pixel color for the arrow. -

    widgetMaxItemLen

    -

    Scan an array of strings and return the length (in characters) of the longest item.

    -
    int32_t widgetMaxItemLen(const char **items, int32_t count);
    +

    widgetMaxItemWidth

    +

    Scan an array of strings and return the width in pixels of the widest item in a font.

    +
    int32_t widgetMaxItemWidth(const BitmapFontT *font, const char **items, int32_t count);
      Parameter   Description
       ---------   -----------
    +  font        Font the items are drawn in (fixed-pitch or proportional).
       items       Array of null-terminated string pointers (NULL entries count as empty).
       count       Number of items in the array.
    -

    Returns the character length of the longest item, or 0 if count is zero.

    +

    Returns the width of the widest item, or 0 if count is zero.

    +

    listTextFit

    +

    How many leading characters of text fit whole in maxW pixels of font: the part of an item a list draws when the item is cut to its column. Stops at the end of the text; 0 when maxW is too narrow for the first character.

    +
    int32_t listTextFit(const BitmapFontT *font, const char *text, int32_t maxW);

    widgetNavigateIndex

    Compute a new selected index from a navigation key press. Handles Up, Down, Home, End, Page Up, and Page Down.

    int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize);
    @@ -9213,7 +9591,7 @@ void listReorderPaint(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t to pageSize Number of visible items (used for PgUp/PgDn step size).

    Returns the new index, clamped to [0, count-1]. Returns -1 for unrecognized keys so callers can detect whether the key was consumed.

    widgetGridNavigateIndex

    -

    The same keys in a grid of count items laid out row by row, columns items per row (icon views): Left and Right step through the items in order, Up, Down, Page Up and Page Down move by rows (pageRows rows a page) the way widgetNavigateIndex moves through a list -- it is called on the row numbers -- keeping the column, or landing on the last item when the row below is shorter; Home and End go to the first and last item. With nothing selected (current below 0) every one of these keys goes to the first item. With one column it gives what widgetNavigateIndex gives.

    +

    The same keys in a grid of count items laid out row by row, columns items per row (icon views): Left and Right step through the items in order, Up, Down, Page Up, and Page Down move by rows (pageRows rows a page) the way widgetNavigateIndex moves through a list -- it is called on the row numbers -- keeping the column, or landing on the last item when the row below is shorter; Home and End go to the first and last item. With nothing selected (current below 0) every one of these keys goes to the first item. With one column it gives what widgetNavigateIndex gives.

    int32_t widgetGridNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t columns, int32_t pageRows);

    Returns the new index, or -1 for any other key.

    widgetDropdownPopupRect

    @@ -9276,7 +9654,7 @@ void listReorderPaint(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t to scrollPos [in/out] Current scroll offset; updated in place when the click hits the scrollbar.

    Returns true if the click landed on the scrollbar (even if scrollPos was not changed, for example clicking the thumb), false if the click was in the item list area. When itemCount is less than or equal to DROPDOWN_MAX_VISIBLE, always returns false (there is no scrollbar).

    Icon Views

    -

    The rules of an icon view with large icons, small icons or a list: Program Manager's view (progman/pmView.c) is built on them, and so are icon views written in BASIC (listhelp.bas ListIcon...), so both look and behave alike. They cover each view's cell and where an item's picture and name go in it, the grid inside a sunken frame with a scroll bar only when the rows do not fit (which narrows the grid), the two label lines under a large icon, the arrow keys among items in order or in cells of their own (gaps allowed), the scroll bar (a control's, as ListBox's: arrows, trough, thumb drag), when a press becomes a drag, the scrolling and the target cell while dragging, and where a dropped item lands. Positions are pixels from the view's top left (ListIconGridT) or the cell's (ListIconCellT).

    +

    The rules of an icon view with large icons, small icons, or a list: Program Manager's view (progman/pmView.c) is built on them, and so are icon views written in BASIC (listhelp.bas ListIcon...), so both look and behave alike. They cover each view's cell and where an item's picture and name go in it, the grid inside a sunken frame with a scroll bar only when the rows do not fit (which narrows the grid), the two label lines under a large icon, the arrow keys among items in order or in cells of their own (gaps allowed), the scroll bar (a control's, as ListBox's: arrows, trough, thumb drag), when a press becomes a drag, the scrolling and the target cell while dragging, and where a dropped item lands. Positions are pixels from the view's top left (ListIconGridT) or the cell's (ListIconCellT).

      Constant                 Value  Description
       --------                 -----  -----------
       LIST_ICON_FRAME          2      Sunken frame around the view.
    @@ -9345,13 +9723,14 @@ int32_t listIconScrollDrag(const ListIconGridT *g, int32_t y, int32_t grab);
    +

    charW and charH are the caller's character metrics (a font's charWidth and charHeight, or numbers from BASIC, which has no font handle here): rows are a line tall, and a name field or column N characters wide is N * charW. A large icon's label is cut by characters (nameChars, listIconLabelSplit): exact for a fixed-pitch font, an estimate for a proportional one.

    Program Manager's pmView.c draws the scroll bar with widgetDrawScrollbarV; a BASIC icon view draws the same bar with DrawScrollBar's control argument True.

    @@ -9385,7 +9764,7 @@ int32_t listIconScrollDrag(const ListIconGridT *g, int32_t y, int32_t grab); -

    An icon view's grid is figured from the view's size on each call (no handle). The LISTSEL_KEY_ and LISTICON_ CONSTs of listhelp.bas are compared with ListSelKeyE, ListIconViewE, LIST_ICON_BY_ORDER and ListIconMetricE by a formrt test.

    +

    An icon view's grid is figured from the view's size on each call (no handle). The LISTSEL_KEY_ and LISTICON_ CONSTs of listhelp.bas are compared with ListSelKeyE, ListIconViewE, LIST_ICON_BY_ORDER, and ListIconMetricE by a formrt test.

    Music Library

    @@ -9401,7 +9780,7 @@ if (rc != MUSIC_OK) { musicSetEndFn(tune, onTuneEnd, NULL); musicPlay(tune, false); // true: loop until stopped }
    -

    musicLoad and musicLoadMem tell the kind by the contents: a MIDI file (or a RIFF RMID file around one) by its header, anything else is offered to the module player. Errors: MUSIC_ERR_OPEN (unreadable file), MUSIC_ERR_FORMAT (not music DVX plays, or a type 2 MIDI file), MUSIC_ERR_CORRUPT, MUSIC_ERR_NO_MEMORY, MUSIC_ERR_NO_DEVICE (no sound device could start), MUSIC_ERR_BANK (MIDI with no usable instrument bank and no MPU-401 port). Nothing plays until musicPlay(music, loop), which plays from the current position (the start after a load or a stop; a music that played to its end starts over). musicPause / musicResume, musicStop (back to the start), musicIsPlaying, musicSetVolume (0..MUSIC_VOLUME_MAX, per music; the master volume applies too - on the FM chip and the MPU-401 port as it was when the music started or its volume was last set), musicGetPosition / musicSetPosition (milliseconds), musicGetInfo (kind, where it plays, a description of the device, title, length, position, state) and musicFree complete the set. musicSetEndFn(music, fn, userData) asks for a call when a music that does not loop plays to its end; it comes in the shell's idle time as the program that owns the music, never from an interrupt.

    +

    musicLoad and musicLoadMem tell the kind by the contents: a MIDI file (or a RIFF RMID file around one) by its header, anything else is offered to the module player. Errors: MUSIC_ERR_OPEN (unreadable file), MUSIC_ERR_FORMAT (not music DVX plays, or a type 2 MIDI file), MUSIC_ERR_CORRUPT, MUSIC_ERR_NO_MEMORY, MUSIC_ERR_NO_DEVICE (no sound device could start), MUSIC_ERR_BANK (MIDI with no usable instrument bank and no MPU-401 port). Nothing plays until musicPlay(music, loop), which plays from the current position (the start after a load or a stop; a music that played to its end starts over). musicPause / musicResume, musicStop (back to the start), musicIsPlaying, musicSetVolume (0..MUSIC_VOLUME_MAX, per music; the master volume applies too - on the FM chip and the MPU-401 port as it was when the music started or its volume was last set), musicGetPosition / musicSetPosition (milliseconds), musicGetInfo (kind, where it plays, a description of the device, title, length, position, state), and musicFree complete the set. musicSetEndFn(music, fn, userData) asks for a call when a music that does not loop plays to its end; it comes in the shell's idle time as the program that owns the music, never from an interrupt.

    Setting the position of a MIDI file sends the instruments and controllers set before that point (no notes). A module goes to the start of the pattern that holds the time.

    Where Music Plays

    Modules are rendered by libxmp-lite into a stream of the sound device (16-bit stereo at the device's rate, 400 ms of buffer, linear interpolation), so they play on any device that plays sampled sound.

    @@ -9414,10 +9793,10 @@ if (rc != MUSIC_OK) { software the software synthesizer

    On the FM chip and the MPU-401 port the song is played by the timer service (500 times a second), so it keeps time however busy the programs are. On the FM chip every note takes a voice from audio.lib's shared pool; when none is free the note plays on the software synthesizer instead, which then runs beside the chip (its stream is always open for that). The software synthesizer runs in the stream's fill function (the shell's idle time), 64 frames between event checks, 150 ms ahead of what is heard; a program that keeps the desktop busy every frame should call audioPump once a frame, as for any stream.

    General MIDI on FM

    -

    Instruments come from an OP2 bank (the DMX "GENMIDI" format: 128 melodic instruments and 47 percussion instruments for notes 35 to 81, each one or two two-operator voices, with a note offset, a fixed note for percussion and a detune for the second voice). DVX ships Freedoom's GENMIDI bank (see Licences below); DVX.INI [audio] midiBank names another OP2 file instead (a path from the DVX folder), for example a bank made with an OPL editor, or another freely licensed bank such as DMXOPL. The bank is read once, at the first MIDI music that needs it.

    -

    What is played: note on / off, velocity, program change, channel volume (CC7), expression (CC11), pan (CC10: left, both or right on an OPL3 and the software synthesizer), sustain pedal (CC64), pitch bend with the bend range from RPN 0 (default 2 semitones), all notes off (CC123, CC120), reset controllers (CC121), and General MIDI System On (a reset). Channel 10 plays percussion. Loudness is velocity x volume x expression x the music's volume on a 40 log10 curve, added as attenuation to the voice's level. OP2 banks are tuned for DMX, which sounds note n where MIDI has note n + 12, so notes are played an octave up from the bank's offsets. Not played: modulation (CC1), aftertouch, the FM chip's own rhythm mode, four-operator voices.

    +

    Instruments come from an OP2 bank (the DMX "GENMIDI" format: 128 melodic instruments and 47 percussion instruments for notes 35 to 81, each one or two two-operator voices, with a note offset, a fixed note for percussion, and a detune for the second voice). DVX ships Freedoom's GENMIDI bank (see Licences below); DVX.INI [audio] midiBank names another OP2 file instead (a path from the DVX folder), for example a bank made with an OPL editor, or another freely licensed bank such as DMXOPL. The bank is read once, at the first MIDI music that needs it.

    +

    What is played: note on / off, velocity, program change, channel volume (CC7), expression (CC11), pan (CC10: left, both, or right on an OPL3 and the software synthesizer), sustain pedal (CC64), pitch bend with the bend range from RPN 0 (default 2 semitones), all notes off (CC123, CC120), reset controllers (CC121), and General MIDI System On (a reset). Channel 10 plays percussion. Loudness is velocity x volume x expression x the music's volume on a 40 log10 curve, added as attenuation to the voice's level. OP2 banks are tuned for DMX, which sounds note n where MIDI has note n + 12, so notes are played an octave up from the bank's offsets. Not played: modulation (CC1), aftertouch, the FM chip's own rhythm mode, four-operator voices.

    The Software Synthesizer

    -

    When there is no FM chip (or it is busy), MIDI plays on DVX's own software FM synthesizer: an integer two-operator FM chip modelled on the OPL2 (the same log-sine and exponential tables, waveforms 0 to 3, feedback, the chip's envelope rates and key scaling, key scale level, tremolo and vibrato) with 24 voices, rendering at the device's rate into a stream. It is driven with the same bank and the same register values as the chip, so music sounds alike on both. The host tests compare it with emu8950, an accurate OPL2 emulator, for several patches: pitch, loudness over time and harmonics agree closely. Cost on a 486: roughly 1 to 2 percent of a 486DX2-66 per sounding voice at 22,050 Hz.

    +

    When there is no FM chip (or it is busy), MIDI plays on DVX's own software FM synthesizer: an integer two-operator FM chip modelled on the OPL2 (the same log-sine and exponential tables, waveforms 0 to 3, feedback, the chip's envelope rates and key scaling, key scale level, tremolo, and vibrato) with 24 voices, rendering at the device's rate into a stream. It is driven with the same bank and the same register values as the chip, so music sounds alike on both. The host tests compare it with emu8950, an accurate OPL2 emulator, for several patches: pitch, loudness over time, and harmonics agree closely. Cost on a 486: roughly 1 to 2 percent of a 486DX2-66 per sounding voice at 22,050 Hz.

    The MPU-401 Port

    With DVX.INI [audio] midi = mpu401, MIDI goes out of the MPU-401 port (audio.lib's mpu401 driver) to an external synthesizer, one music at a time (a second MIDI music meanwhile plays on the FM chip or the software synthesizer). Bytes are queued and sent by the timer as fast as the port takes them, with a 40 ms pause after every system exclusive message (first-generation MT-32s lose data without it). The music's volume scales every channel volume (CC7) sent. DVX.INI [audio] mpuMode says what is on the port:

      mpuMode    Treatment
    @@ -9425,7 +9804,7 @@ if (rc != MUSIC_OK) {
       gm         A General MIDI synthesizer (Roland SC-55 and the like): the music as it is, after a General MIDI System On message.
       mt32       A Roland MT-32 (or CM-32L, LAPC-I) playing General MIDI music: the melodic channels move to the MT-32's eight parts (MIDI channels 2 to 9, in the order they are first used; a ninth channel shares the last part), percussion stays on channel 10, programs go through a General MIDI to MT-32 timbre table (our own nearest-sound choice), pan is mirrored (the MT-32 pans the other way), the bend range is set to 2 semitones by system exclusive messages at the start (and RPN 0 changes it the same way, since the MT-32 has no RPNs), and what it does not understand (other RPNs and NRPNs, bank select, reverb and chorus depth, aftertouch, system exclusive messages that are not Roland's) is left out.
       raw        Anything: the music exactly as it is, nothing added (for music written for an MT-32, with its own set-up messages).
    -

    Stopping, pausing or freeing a music sends sustain off and all notes off on every channel the synthesizer hears.

    +

    Stopping, pausing, or freeing a music sends sustain off and all notes off on every channel the synthesizer hears.

    Live MIDI

    MusicT *keys = NULL;
     
    @@ -9433,21 +9812,21 @@ musicMidiOpen(&keys);                          // plays at once
     musicMidiProgram(keys, 0, 73);                 // flute on channel 0
     musicMidiNoteOn(keys, 0, 60, 100);             // middle C
     musicMidiNoteOff(keys, 0, 60);
    -

    musicMidiOpen opens a music with no file that plays at once, on the same synthesizer a MIDI file would use. musicMidiNoteOn, musicMidiNoteOff, musicMidiProgram, musicMidiControl and musicMidiPitchBend (-8192..8191) send channel messages (channels 0 to 15, data 0 to 127) to it, or to a MIDI file that is playing (for dynamic game scores); MUSIC_ERR_ARG for a module or a value out of range.

    +

    musicMidiOpen opens a music with no file that plays at once, on the same synthesizer a MIDI file would use. musicMidiNoteOn, musicMidiNoteOff, musicMidiProgram, musicMidiControl, and musicMidiPitchBend (-8192..8191) send channel messages (channels 0 to 15, data 0 to 127) to it, or to a MIDI file that is playing (for dynamic game scores); MUSIC_ERR_ARG for a module or a value out of range.

    Modules

    -

    MOD (ProTracker and compatibles), S3M, XM and IT through libxmp-lite 4.7.3 (thirdparty/libxmp-lite, unmodified; formats only, no depackers or ProWizard). Stereo separation is 50 percent (softer than the Amiga's hard left and right). The whole module is held in memory. libxmp works out pitches with floating point, so modules play in the shell's idle time, never at interrupt time; on a 486SX (no maths coprocessor) that goes through floating point emulation and is slow. Rough cost on a 486DX2-66 at 22,050 Hz: 5 percent for a 4-channel MOD, 20 to 25 percent for a 16-channel XM.

    +

    MOD (ProTracker and compatibles), S3M, XM, and IT through libxmp-lite 4.7.3 (thirdparty/libxmp-lite, unmodified; formats only, no depackers or ProWizard). Stereo separation is 50 percent (softer than the Amiga's hard left and right). The whole module is held in memory. libxmp works out pitches with floating point, so modules play in the shell's idle time, never at interrupt time; on a 486SX (no maths coprocessor) that goes through floating point emulation and is slow. Rough cost on a 486DX2-66 at 22,050 Hz: 5 percent for a 4-channel MOD, 20 to 25 percent for a 16-channel XM.

    Music Macros

    MusicMmlT *tune = NULL;
     
     musicMmlOpen(&tune);
     musicMmlPlay(tune, "MB T140 O3 L8 CDEC CDEC EFG4 EFG4");

    A macro queue plays tunes written in QBasic's music macro language (the commands are listed in music.h and in the BASIC language reference: notes A-G with sharps and flats, N, O, > and <, L, P, dots, T, MN / ML / MS, MF / MB). musicMmlPlay parses the whole string first; a mistake anywhere returns MUSIC_ERR_MACRO and queues nothing. The settings last from one string to the next. musicMmlTone queues a square-wave tone of milliHz thousandths of a hertz (0: silence) for us microseconds (QBasic's SOUND). musicMmlQueued counts the notes not yet played, musicMmlStop silences the queue, musicMmlBusy says whether a caller that follows QBasic should still wait (foreground mode: until everything has played; background: while more than MUSIC_MML_BACKGROUND_MAX notes wait) and moves the queue along. A queue belongs to the program that opened it and is freed when that program ends.

    -

    Timing is QBasic's: a whole note is four beats of 60 / T seconds, each dot adds half again, and a note sounds for 7/8 (MN), all (ML) or 3/4 (MS) of its length. Octave 3 starts at middle C; octave 3's A is 440 Hz. The notes play as square waves on an audio.lib note queue, each to the exact sample (on the PC speaker as tones). DVX's own I command (I1 to I128) plays the notes that follow on that General MIDI instrument: each run of such notes becomes a small MIDI file that plays like any MIDI music (on the synthesizer DVX.INI [audio] midi chooses); I0 returns to tones. Runs play one after the other; when the instruments cannot play (no instrument bank, no device) the run plays as tones.

    +

    Timing is QBasic's: a whole note is four beats of 60 / T seconds, each dot adds half again, and a note sounds for 7/8 (MN), all (ML), or 3/4 (MS) of its length. Octave 3 starts at middle C; octave 3's A is 440 Hz. The notes play as square waves on an audio.lib note queue, each to the exact sample (on the PC speaker as tones). DVX's own I command (I1 to I128) plays the notes that follow on that General MIDI instrument: each run of such notes becomes a small MIDI file that plays like any MIDI music (on the synthesizer DVX.INI [audio] midi chooses); I0 returns to tones. Runs play one after the other; when the instruments cannot play (no instrument bank, no device) the run plays as tones.

    Several Programs at Once

    Every music plays through one audio context of the library's own; audio.lib mixes all streams, so modules and software MIDI of any number of programs play together. FM voices are shared with every program's tones and FM music; the MPU-401 port serves one music. When a program ends, its musics stop and are freed and their FM voices and the port are given back; the others go on.

    BASIC

    src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName) and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums). A number only works for the program that got it. MusicGetVolume(music) (musicGetVolume in percent) and MusicInfo$(music, field) (musicGetInfo: "title", "output", "kind" = "module" / "midi" / "live" for MusicKindE, "where" = "" / "stream" / "mpu401" / "fm" / "software" for MusicOutputE, "paused" and "looping" as "-1" / "0"; any case, "" for another field). music.bas also declares the raw MIDI port calls of audio.lib (MidiPortInfo$, MidiAcquire, MidiPut, MidiRelease; see the sound library).

    -

    The BASIC runtime itself calls musicBasBeep, musicBasSound(hz, ticks), musicBasPlay(macro), musicBasPlayCount and musicBasBusy for the statements BEEP, SOUND, PLAY and the PLAY(n) function; each BASIC program has its own macro queue. QBasic's X command and "=" numbers (given with VARPTR$) are filled in by the BASIC runtime before musicBasPlay is called, so the macro language itself has neither. SOUND's duration is in clock ticks (18.2 a second) and 0 stops the queue; BEEP is 800 Hz for a quarter second.

    +

    The BASIC runtime itself calls musicBasBeep, musicBasSound(hz, ticks), musicBasPlay(macro), musicBasPlayCount, and musicBasBusy for the statements BEEP, SOUND, and PLAY and the PLAY(n) function; each BASIC program has its own macro queue. QBasic's X command and "=" numbers (given with VARPTR$) are filled in by the BASIC runtime before musicBasPlay is called, so the macro language itself has neither. SOUND's duration is in clock ticks (18.2 a second) and 0 stops the queue; BEEP is 800 Hz for a quarter second.

    Licences

    libxmp-lite (Extended Module Player Lite), (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr, MIT licence:

    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    @@ -9456,11 +9835,11 @@ musicMmlPlay(tune, "MB T140 O3 L8 CDEC CDEC EFG4 EFG4");

    Printing Library

    -

    Printer ports and print jobs, text laid out on pages, pictures for dot-matrix and laser printers, the printer settings and the common Print and Page Setup dialogs. Notepad, Image Viewer, Icon Editor, the Control Panel's Printer page and BASIC's LPRINT all print through it; any program can.

    +

    Printer ports and print jobs, text laid out on pages, pictures for dot-matrix and laser printers, the printer settings, and the common Print and Page Setup dialogs. Notepad, Image Viewer, Icon Editor, the Control Panel's Printer page, and BASIC's LPRINT all print through it; any program can.

    Header: print/print.h

    Loaded as: bin/libs/kpunch/print/print.lib

    Settings

    -

    The printer is chosen once for everything, in DVX.INI [printer] (the Control Panel's Printer page and the Page Setup dialog edit it): where output goes (PrintDestE: LPT1, LPT2, LPT3 or a file), the printer model (PrintModelE: text only, Epson 9-pin, Epson 24-pin, HP LaserJet) and the page setup (PrintPageSetupT).

    +

    The printer is chosen once for everything, in DVX.INI [printer] (the Control Panel's Printer page and the Page Setup dialog edit it): where output goes (PrintDestE: LPT1, LPT2, LPT3, or a file), the printer model (PrintModelE: text only, Epson 9-pin, Epson 24-pin, HP LaserJet), and the page setup (PrintPageSetupT).

      Function                                   Description
       --------                                   -----------
       printSettingsLoad(&s)                      Read DVX.INI into a PrintSettingsT (defaults for anything missing; a page setup that cannot work is replaced by the defaults as a whole).
    @@ -9472,9 +9851,9 @@ musicMmlPlay(tune, "MB T140 O3 L8 CDEC CDEC EFG4 EFG4");
    printDestChoices[] / printModelChoices[] The labels and INI spellings (PrefsChoiceT), indexed by PrintDestE / PrintModelE. printDestLabel(d) / printModelLabel(m) The label of one choice (for programs that look the library up by name). printDeviceProblem(dest, file) NULL when the destination can be used, else why not (printing to a file needs its name). - printDeviceStore(h, model, dest, file) Put model, destination and file into the [printer] section of h with the page setup as it is in DVX.INI now (the caller saves h). The Control Panel's OK and print.bas use it. + printDeviceStore(h, model, dest, file) Put model, destination, and file into the [printer] section of h with the page setup as it is in DVX.INI now (the caller saves h). The Control Panel's OK and print.bas use it.

    Jobs and Printer Problems

    -

    A job sends bytes to a printer port or into a file. On DOS a port is driven through the BIOS printer service (INT 17h); a byte is sent only when a status check says the printer is ready, so the BIOS never waits by itself. While the printer is busy the job calls the idle hook; a printer busy for PRINT_BUSY_TIMEOUT_MS (10 seconds), out of paper, off line or reporting an error is a problem, and so is a file that cannot be written (disk full). A problem goes to the ask hook: Retry tries again, Cancel ends the job and every later write fails at once.

    +

    A job sends bytes to a printer port or into a file. On DOS a port is driven through the BIOS printer service (INT 17h); a byte is sent only when a status check says the printer is ready, so the BIOS never waits by itself. While the printer is busy the job calls the idle hook; a printer busy for PRINT_BUSY_TIMEOUT_MS (10 seconds), out of paper, off line, or reporting an error is a problem, and so is a file that cannot be written (disk full). A problem goes to the ask hook: Retry tries again, Cancel ends the job and every later write fails at once.

    PrintHooksT hooks = { myAsk, myIdle, NULL, myState };
     char        err[PRINT_MSG_MAX];
     PrintJobT  *job = printJobOpen(PrintDestLpt1E, NULL, &hooks, err, sizeof(err));
    @@ -9490,10 +9869,10 @@ if (!job) {
       ask(ctx, message)             For a problem; return PrintAnswerRetryE or PrintAnswerCancelE. NULL cancels.
       idle(ctx)                     While the printer is busy, and after every PRINT_YIELD_BYTES bytes (4096 into a file). Return false to cancel. NULL calls tsYield, so other programs keep running.
       finished(ctx, closing)        When a write that called idle returns, and (closing true) when the job is freed.
    -

    printJobWrite returns once every byte was taken, or false when the job is cancelled. printJobBytes gives the count sent, printJobCancelled whether it was cancelled. printStatusProblem(status, afterWrite, where, out, size) turns BIOS status bits (PRINT_STATUS_*) into the message the user sees. printSetPortOps replaces the port services (present / status / write) with your own, as the tests do to play a printer that is busy or out of paper; NULL restores the real ones. Elsewhere than DOS the real ports always exist, are always ready and append to lpt1.prn .. lpt3.prn in the working directory.

    +

    printJobWrite returns once every byte was taken, or false when the job is cancelled. printJobBytes gives the count sent, printJobCancelled whether it was cancelled. printStatusProblem(status, afterWrite, where, out, size) turns BIOS status bits (PRINT_STATUS_*) into the message the user sees. printSetPortOps replaces the port services (present / status / write) with your own, as the tests do to play a printer that is busy or out of paper; NULL restores the real ones. Elsewhere than DOS the real ports always exist, are always ready, and append to lpt1.prn .. lpt3.prn in the working directory.

    Text

    printTextJob(job, model, text, len, &pageSetup, &info) prints plain text on pages. Lines end with LF or CR LF; a form feed in the text starts a new page. Each printed line starts with marginLeft blanks and ends with CR LF, tabs are expanded to tabWidth, other control characters print as blanks, and a line longer than the text width is wrapped at the last blank before it (a word longer than a whole line is split) or, with wrap off, cut off. Every page ends with a form feed. For the Epson and LaserJet models the printer's reset command comes first (and, for the LaserJet, last).

    -

    A page is marginTop lines with the header in their middle line, the text, then marginBottom lines with the footer in their middle line. Header and footer codes: &f file name (PrintTextInfoT.title), &p page number, &n number of pages, &d date, &t time (PrintTextInfoT.date / time, or today and now), && an ampersand; &l, &c and &r put what follows at the left, centre or right (text before any of them is centred). printFormatHeader expands a header for one page.

    +

    A page is marginTop lines with the header in their middle line, the text, then marginBottom lines with the footer in their middle line. Header and footer codes: &f file name (PrintTextInfoT.title), &p page number, &n number of pages, &d date, &t time (PrintTextInfoT.date / time, or today and now), && an ampersand; &l, &c, and &r put what follows at the left, centre, or right (text before any of them is centred). printFormatHeader expands a header for one page.

    PrintTextInfoT also chooses a page range (firstPage, lastPage; 0 = all) and the number of copies. printTextPageCount returns the number of pages text takes.

    Pictures and Printer Drivers

    printRasterJob(job, driver, rgb, w, h, pitch) prints a picture of R, G, B bytes on one page: scaled to the largest size that fits the printable area (8 by 10 inches) keeping its shape, and dithered to black and white with a 4x4 ordered pattern. The driver gets one band of dot rows at a time, so only one band is ever in memory. printDriver(model) returns the driver of a model (NULL for text only).

    @@ -9509,13 +9888,13 @@ if (!job) { printDialog(ctx, title, pageCount, &choice) The common Print dialog: print to a port or a file, copies and, when pageCount > 0, all pages or a range. Starts from the settings; true with the PrintChoiceT filled when the user chose Print. printPageSetupDialog(ctx) The Page Setup dialog; saves the page setup to DVX.INI. printUiText(ctx, text, title, askFirst) Print text with the page setup on the printer in the settings (the Print dialog first when askFirst). - printUiTextChoice(ctx, text, title, &choice) Print text on the destination, copies and page range of a PrintChoiceT (a Print dialog answer, or one the program filled) without asking; printUiText ends here. + printUiTextChoice(ctx, text, title, &choice) Print text on the destination, copies, and page range of a PrintChoiceT (a Print dialog answer, or one the program filled) without asking; printUiText ends here. printUiImageFile(ctx, path, title, askFirst) Print a picture file (BMP, PNG, JPEG, GIF). printUiWindow(ctx, win, askFirst) Print a window's contents as a picture. printUiOpenPort(ctx, port) A job with the user interface hooks on LPTn (0-based) or PRINT_PORT_DEFAULT.

    Jobs opened by these ask about printer problems with a Retry / Cancel message box. While a write waits, a modal Printing window with a Cancel button appears (at once for a document, after half a second for other output) and the whole desktop keeps running under it. Problems are told to the user; the functions return true only when everything was printed. Pictures need a printer model that prints them; on a text-only printer the user is told to choose the model in the Control Panel.

    BASIC

    -

    src/include/basic/print.bas declares PrintTextDialog, PrintText, PrintPictureDialog, PrintPicture, PrintPageSetup and PrintCanPrintPictures. For the printer settings it also declares PrintModelCount / PrintModelLabel$, PrintDestCount / PrintDestLabel$ / PrintDestIsFile, PrintSettingModel / PrintSettingDest / PrintSettingFile$ (printSettingsRead on a prefs.bas handle, through prefsBasHandle), PrintSettingsProblem$ (printDeviceProblem) and PrintSettingsStore (printDeviceStore).

    +

    src/include/basic/print.bas declares PrintTextDialog, PrintText, PrintPictureDialog, PrintPicture, PrintPageSetup, and PrintCanPrintPictures. For the printer settings it also declares PrintModelCount / PrintModelLabel$, PrintDestCount / PrintDestLabel$ / PrintDestIsFile, PrintSettingModel / PrintSettingDest / PrintSettingFile$ (printSettingsRead on a prefs.bas handle, through prefsBasHandle), PrintSettingsProblem$ (printDeviceProblem), and PrintSettingsStore (printDeviceStore).

      BASIC                                             C it calls
       -----                                             ----------
       PrintPageSetting(prefs, field), PrintPageText$(prefs, field)  printSettingsRead on a prefs.bas handle: one PrintPageSetupT field by name (Lines, Columns, MarginTop, MarginBottom, MarginLeft, MarginRight, TabWidth, Wrap; Header, Footer).
    @@ -9528,7 +9907,74 @@ if (!job) {
       PrintForm(form), PrintFormDialog(form)            printUiWindow on a loaded form's window (these two live in the BASIC runtime, which knows the forms).
       PrintPortPresent(port), PrintPortStatus(port)     printPortPresent / printPortStatus; port 1-3 is LPT1-LPT3.
       PrintStatusProblem$(status, afterWrite, where)    printStatusProblem ("" when the status shows none).
    -

    print.bas also has CONSTs for the PRINT_STATUS_ bits and the page setup limits and defaults (PRINT_LINES_STANDARD ... PRINT_COPIES_MAX); a formrt test compares them with print.h. The BASIC runtime gives the library the GUI context (printBasSetContext) when a program starts, and runs LPRINT, LPRINT USING and OPEN "LPT1:" / "PRN" through printUiOpenPort, printJobWrite and printJobClose. See the BASIC language reference (LPRINT, File I/O, Include Libraries).

    +

    print.bas also has CONSTs for the PRINT_STATUS_ bits and the page setup limits and defaults (PRINT_LINES_STANDARD ... PRINT_COPIES_MAX); a formrt test compares them with print.h. The BASIC runtime gives the library the GUI context (printBasSetContext) when a program starts, and runs LPRINT, LPRINT USING, and OPEN "LPT1:" / "PRN" through printUiOpenPort, printJobWrite, and printJobClose. See the BASIC language reference (LPRINT, File I/O, Include Libraries).

    +
    +
    +

    Styled Text Library

    +

    Styled text and RTF in and out: the document of the RichEdit widget, and of its undo steps and clipboard. A RichTextT is text in code page 437 with '\n' between paragraphs, covered by runs -- each from an offset to the next run's, in a style (font family, pixel size, bold, italic, underline, strike-through, and color) -- and one alignment per paragraph. It needs no widget: a program can build, edit, and convert styled text with it alone.

    +

    Header: richtext/richText.h

    +

    Loaded as: bin/libs/richtext.lib

    +

    Types

    +
      Type          Fields
    +  ----          ------
    +  RichStyleT    family (FONT_FAMILY_MAX; "" = the default family), size (pixels; 0 = the default size), style (FONT_STYLE_*), color (0x00RRGGBB or RICH_COLOR_DEFAULT)
    +  RichRunT      start (the first character's offset), style (an index in RichTextT.styles)
    +  RichTextT     text, runs, styles, aligns: stb_ds arrays; the text has no NUL, the first run starts at 0, styles[0] is the default style, one alignment (FLOW_ALIGN_*) per paragraph
    +  RichChangeT   set (RICH_SET_FAMILY, RICH_SET_SIZE, RICH_SET_COLOR), styleMask (the FONT_STYLE_* bits to set to to.style's), to (the new values)
    +

    The default family, size, and color are a RichEdit control's own font and ForeColor, so text in them follows the control. Styles are interned: one look is one index, so neighboring runs never share a style and no run covers nothing (but the one of an empty text). Running out of memory ends the program, as stb_ds does everywhere in DVX.

    +

    Constants

    +
      Constant             Value                Description
    +  --------             -----                -----------
    +  RICH_COLOR_DEFAULT   FLOW_COLOR_DEFAULT   A run in the default color
    +  RICH_TAB_SPACES      4                    The spaces a tab in plain text or RTF becomes (DVX text has no tabs)
    +  RICH_SET_FAMILY      0x0001               RichChangeT.set: change the family
    +  RICH_SET_SIZE        0x0002               RichChangeT.set: change the size
    +  RICH_SET_COLOR       0x0004               RichChangeT.set: change the color
    +

    Styled Text

    +
    void       richTextInit(RichTextT *rt);
    +void       richTextClear(RichTextT *rt);
    +void       richTextFree(RichTextT *rt);
    +int32_t    richTextLength(const RichTextT *rt);
    +int32_t    richTextIntern(RichTextT *rt, const RichStyleT *style);
    +int32_t    richTextRunAt(const RichTextT *rt, int32_t offset);
    +int32_t    richTextStyleAt(const RichTextT *rt, int32_t offset);
    +int32_t    richTextParaCount(const RichTextT *rt);
    +int32_t    richTextParaOf(const RichTextT *rt, int32_t offset);
    +int32_t    richTextParaStart(const RichTextT *rt, int32_t para);
    +int32_t    richTextParaEnd(const RichTextT *rt, int32_t para);
    +int32_t    richTextAlign(const RichTextT *rt, int32_t para);
    +void       richTextSetAlign(RichTextT *rt, int32_t first, int32_t last, int32_t align);
    +void       richTextCopy(const RichTextT *src, int32_t from, int32_t to, RichTextT *dst);
    +int32_t    richTextReplace(RichTextT *rt, int32_t from, int32_t to, const RichTextT *what);
    +int32_t    richTextInsertPlain(RichTextT *rt, int32_t at, const char *text, int32_t len, int32_t style);
    +void       richTextFromPlain(RichTextT *rt, const char *text, int32_t len, const RichStyleT *style);
    +void       richTextRestyle(RichTextT *rt, int32_t from, int32_t to, const RichChangeT *change);
    +RichStyleT richStyleChanged(const RichStyleT *style, const RichChangeT *change);
    +
      Function               Description
    +  --------               -----------
    +  richTextInit / Clear   An empty text: one empty, left-aligned paragraph in the default style. Free releases the arrays.
    +  richTextIntern         The index of a style, added when the text has none like it.
    +  richTextRunAt          The run the character at an offset is in (at the end, the last run); richTextStyleAt its style.
    +  richTextPara*          Paragraph count; the paragraph an offset is in (its '\n' belongs to it); where one starts and ends (its '\n' or the text's end); its alignment.
    +  richTextCopy           A range as a text of its own: its characters, runs, the styles they use, and the alignments of the paragraphs it touches.
    +  richTextReplace        Replace a range with a text (NULL deletes). Joined paragraphs keep the first one's alignment; each paragraph the new text starts takes its alignment from it. Returns the length inserted.
    +  richTextInsertPlain    Insert plain text in a style: '\r' dropped, a tab as RICH_TAB_SPACES spaces, '\n' starting a paragraph aligned as the one it is in.
    +  richTextFromPlain      The whole text as plain text in one style, made the default style.
    +  richTextRestyle        Apply a RichChangeT to a range; richStyleChanged applies one to a single style.
    +

    Undo works by these: before a change, richTextCopy saves the range it replaces; undoing replaces what the change put there with the saved copy. A copy taken, deleted, and put back gives the text it came from exactly -- characters, runs, styles, and alignments.

    +

    RTF

    +
    bool  richRtfIs(const char *text, int32_t len);
    +char *richRtfWrite(const RichTextT *rt, const char *defaultFamily, int32_t *outLen);
    +bool  richRtfRead(RichTextT *rt, const char *text, int32_t len);
    +

    richRtfWrite returns a NUL-terminated RTF document (malloc'd; NULL when out of memory): a font table whose font 0 is defaultFamily (the default family is written as font 0 with no size, and read back as the default), a color table whose entry 0 is the default color, then each paragraph as \pard and its alignment, ended by \par, each run as \plain and its whole style. ASCII characters are themselves; the rest are \uN with a '?' for readers without Unicode. richRtfRead replaces rt with what an RTF document holds and returns false, leaving rt alone, when the text is not RTF.

    +

    The subset read: \fonttbl and \f, \fs (half points; a pixel is taken as 3/4 point, so \fs24 is 16 pixels), \b, \i, \ul, \strike, and their ends, \ulnone, \plain, \colortbl and \cf, \par, \pard, \ql, \qc, \qr (\qj as left; \line as a new paragraph), \tab, \'hh (Windows-1252), \uN with \ucN, the escapes \\ \{ \} \~ \_, and quotes and dashes as their ASCII forms. Characters are stored as code page 437, '?' for those it lacks. Destinations such as \info, \stylesheet, \pict, headers, footers, and every \* group are skipped.

    +
    +
    +

    Styled Text Library: BASIC

    +

    Source: richtext/richTextBas.c; BASIC declarations: src/include/basic/richtext.bas (DECLARE LIBRARY "richtext").

    +
    const char *RtfToText(const char *rtf);
    +const char *TextToRtf(const char *text);
    +

    RtfToText$ is the text of an RTF document, a line per paragraph (text that is not RTF comes back as it is); TextToRtf$ is plain text as RTF in the system font, a paragraph per line. A result stays valid until the next call. The RichEdit control's TextRTF, SelRTF, LoadFile, and SaveFile cover RTF in a control.

    Serial Stack

    @@ -9670,7 +10116,7 @@ if (!job) { rs232GetDsr(com) DSR line state (0 or 1). rs232GetDtr(com) DTR line state (0 or 1). rs232GetRts(com) RTS line state (0 or 1). - rs232GetLsr(com) Line status register value as last read; RS232_LSR_OVERRUN, RS232_LSR_PARITY_ERR, RS232_LSR_FRAME_ERR and RS232_LSR_BREAK are its error bits. + rs232GetLsr(com) Line status register value as last read; RS232_LSR_OVERRUN, RS232_LSR_PARITY_ERR, RS232_LSR_FRAME_ERR, and RS232_LSR_BREAK are its error bits. rs232GetMcr(com) Modem control register value. rs232GetMsr(com) Modem status register value; RS232_MSR_RI is the ring indicator.

    Configuration Setters

    @@ -9687,7 +10133,7 @@ if (!job) { rs232SetDtr(com, dtr) Set DTR line state. The state is kept when the port is closed (dropping DTR is how a modem is hung up). rs232SetBreak(com, on) Hold the transmit line in the break (spacing) state (on = true) or release it. The caller times the break, typically 250-500 ms. rs232SetRts(com, rts) Set RTS line state. The state is kept when the port is closed. - rs232SetFifoThreshold(com, thr) Set FIFO trigger level (1, 4, 8, or 14 bytes) or 0 to turn the FIFO off. Levels other than 0 return RS232_ERR_INVALID_FIFO unless the UART is a 16550A. + rs232SetFifoThreshold(com, thr) Set FIFO trigger level (1, 4, 8, or 14 bytes) or 0 to turn the FIFO off. Levels other than 0 return RS232_ERR_INVALID_FIFO unless the UART is a 16550A. rs232Open sets 8 on a 16550A (the 8 free bytes allow 0.7 ms of interrupt latency at 115200 bps; 14 allows only 0.17 ms) and 0 on other UARTs.

    Buffer Management

      Function                   Description
       --------                   -----------
    @@ -9787,7 +10233,7 @@ void pktSetResetCallback(PktConnT *conn, PktResetCallbackT callback);
     

    Security (DH, XTEA, SHA-256)

    -

    Cryptographic primitives for the serial stack: 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA block cipher in CTR mode, SHA-256, HMAC-SHA256 and HKDF-SHA256, and a SHA-256 based random number generator.

    +

    Cryptographic primitives for the serial stack: 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA block cipher in CTR mode, SHA-256, HMAC-SHA256, and HKDF-SHA256, and a SHA-256 based random number generator.

    XTEA requires no lookup tables and compiles to approximately 20 instructions per round, making it suitable for 486-class hardware where AES S-box tables would thrash the 8KB cache. SHA-256 needs only 32-bit arithmetic and a 256-byte constant table.

    Headers: security/security.h, security/secHash.h

    Constants

    @@ -9835,7 +10281,7 @@ void secHmacUpdate(SecHmacT *h, const uint8_t *data, int32_t len); void secHmacFinal(SecHmacT *h, uint8_t mac[32]); int secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, uint8_t *okm, int32_t okmLen); bool secConstTimeEqual(const void *a, const void *b, int32_t len);
    -

    SHA-256 follows FIPS 180-4, HMAC RFC 2104 and HKDF RFC 5869 (extract and expand; a NULL or empty salt means 32 zero bytes). secHkdf returns SEC_ERR_PARAM for okmLen outside 1..SEC_HKDF_MAX_OUT. secConstTimeEqual compares without revealing where the inputs differ; use it for every MAC check.

    +

    SHA-256 follows FIPS 180-4, HMAC RFC 2104, and HKDF RFC 5869 (extract and expand; a NULL or empty salt means 32 zero bytes). secHkdf returns SEC_ERR_PARAM for okmLen outside 1..SEC_HKDF_MAX_OUT. secConstTimeEqual compares without revealing where the inputs differ; use it for every MAC check.

    Diffie-Hellman Key Exchange

    1024-bit DH with 256-bit private exponents using the RFC 2409 Group 2 safe prime. The modular exponentiation is a Montgomery ladder with branch-free operand swaps, so its timing does not depend on the private key.

    SecDhT *secDhCreate(void);
    @@ -9849,7 +10295,7 @@ void    secDhDestroy(SecDhT *dh);
    secDhCreate Allocate a new DH context. secDhGenerateKeys Generate a private exponent and public key from the DRBG. secDhGetPublicKey Copy the local public key into buf. *len receives the key size. - secDhComputeSecret Compute the shared secret from the remote public key. Keys outside [2, p-2] are rejected with SEC_ERR_PARAM (0, 1 and p-1 would force a trivially known secret). + secDhComputeSecret Compute the shared secret from the remote public key. Keys outside [2, p-2] are rejected with SEC_ERR_PARAM (0, 1, and p-1 would force a trivially known secret). secDhGetSecret Copy the shared secret (SEC_DH_KEY_SIZE big-endian bytes) into buf. It is input for a key derivation function such as secHkdf, never a key by itself. secDhDestroy Securely zero and free the DH context.

    Typical usage order: secDhCreate, secDhGenerateKeys, exchange public keys, secDhComputeSecret, secDhGetSecret, secHkdf, secDhDestroy.

    @@ -9867,7 +10313,7 @@ void secCipherDestroy(SecCipherT *c); secCipherDestroy Securely zero the cipher context (key and counter state) and free its memory.

    secCipherCreate returns NULL if the key pointer is NULL or allocation fails. secCipherCrypt is a no-op when any of c/data is NULL or len <= 0.

    BASIC

    -

    src/include/basic/security.bas (DECLARE LIBRARY "serial") declares the BASIC functions (security/secBas.c). Keys, data and results are BINARY STRING (pointer and length in, a result buffer and length out), so every byte value passes; the result buffer is wiped before it is reused. A handle (1, 2, ...; 0 = none) is a key exchange, a cipher or an incremental SHA-256 / HMAC; each records the program that made it and the shell's app-cleanup hook frees (and wipes) what a program leaves open.

    +

    src/include/basic/security.bas (DECLARE LIBRARY "serial") declares the BASIC functions (security/secBas.c). Keys, data, and results are BINARY STRING (pointer and length in, a result buffer and length out), so every byte value passes; the result buffer is wiped before it is reused. A handle (1, 2, ...; 0 = none) is a key exchange, a cipher, or an incremental SHA-256 / HMAC; each records the program that made it and the shell's app-cleanup hook frees (and wipes) what a program leaves open.

      BASIC                                          C it calls
       -----                                          ----------
       SecRandom$(count), SecAddEntropy bytes         secRngBytes; secRngAddEntropy.
    @@ -9887,7 +10333,7 @@ void        secCipherDestroy(SecCipherT *c);

    widgetTextEditDragUpdateLine

    @@ -10265,7 +10711,7 @@ int main(void) {
    void widgetTextEditDragUpdateLine(TextEditSingleT *te, int32_t vx, int32_t leftEdge, int32_t maxChars, const BitmapFontT *font);
      Parameter     Type                Description
       ---------     ----                -----------
    -  te            TextEditSingleT *   [in/out] Editor state: cursorPos, scrollOff and selEnd are updated.
    +  te            TextEditSingleT *   [in/out] Editor state: cursorPos, scrollOff, and selEnd are updated.
       vx            int32_t             Mouse x position in viewport coordinates.
       leftEdge      int32_t             Pixel x of the left edge of the text area.
       maxChars      int32_t             Maximum visible characters in the field.
    @@ -10283,7 +10729,7 @@ int main(void) {
     
      Parameter     Type                Description
       ---------     ----                -----------
       w             WidgetT *           The widget receiving the click.
    -  te            TextEditSingleT *   [in/out] Editor state: cursorPos, selStart and selEnd are set.
    +  te            TextEditSingleT *   [in/out] Editor state: cursorPos, selStart, and selEnd are set.
       vx            int32_t             Mouse x in viewport coordinates.
       vy            int32_t             Mouse y in viewport coordinates.
       textLeftX     int32_t             Pixel x of the left edge of the text area.
    @@ -10293,12 +10739,12 @@ int main(void) {
     

    widgetTextEditOnKey

    -

    The core single-line text editing engine. Processes a single keystroke and updates the editor's buffer, cursor, scroll offset, selection, and undo state. Shared by TextInput, Spinner and ComboBox.

    +

    The core single-line text editing engine. Processes a single keystroke and updates the editor's buffer, cursor, scroll offset, selection, and undo state. Shared by TextInput, Spinner, and ComboBox.

    void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditSingleT *te, int32_t fieldWidth);
      Parameter     Type                Description
       ---------     ----                -----------
       w             WidgetT *           The widget receiving the key event.
    -  key           int32_t             Key code (ASCII for printable, control code or scan|0x100 for extended).
    +  key           int32_t             Key code (ASCII for printable, control code, or scan|0x100 for extended).
       mod           int32_t             Modifier flags (KEY_MOD_SHIFT, etc.).
       te            TextEditSingleT *   [in/out] Editor state. Ctrl+Z does nothing while undoLen is TEXT_EDIT_UNDO_NONE.
       fieldWidth    int32_t             Field width in pixels including the text padding (subtracted internally). 0 = use widget width.
    @@ -10357,7 +10803,7 @@ int main(void) {

    File Transfer Library

    -

    XMODEM, XMODEM-1K, YMODEM and ZMODEM senders and receivers that work over any byte stream: a serial port, the secure link, and later a network connection. The engines are non-blocking state machines: start a transfer with read and write callbacks, then call xferPoll with the current time in milliseconds as often as you can (every frame). A poll hands queued output to the transport, consumes the input that has arrived and runs the timeouts. Nothing blocks and nothing touches the screen, so a program stays responsive and draws its own progress.

    +

    XMODEM, XMODEM-1K, YMODEM, and ZMODEM senders and receivers that work over any byte stream: a serial port, the secure link, and later a network connection. The engines are non-blocking state machines: start a transfer with read and write callbacks, then call xferPoll with the current time in milliseconds as often as you can (every frame). A poll hands queued output to the transport, consumes the input that has arrived, and runs the timeouts. Nothing blocks and nothing touches the screen, so a program stays responsive and draws its own progress.

    Loaded as: bin/libs/kpunch/xfer/xfer.lib (needs the serial library only for the BASIC functions)

    Header: xfer/xfer.h

    C API

    @@ -10388,7 +10834,7 @@ if (xferPoll(x, nowMs()) != XferRunningE) { XferT Opaque transfer. One XferT runs one transfer at a time and can be started again when it has ended. XferIoT The transport: ctx, read (never blocks; returns the bytes available, 0 when none), write (returns the bytes accepted, may be fewer), pending (optional: bytes accepted but not yet sent; a ZMODEM sender keeps it small so it reacts quickly to an error report on a slow line). XferOptionsT exists (what happens to an incoming file that already exists), rxWindow (ZMODEM receiving: bytes the sender may stream before it waits; 0 = no limit), escapeCtl (ZMODEM: escape every control character), resume (ZMODEM sending: ask the receiver to continue partial files). - XferProgressT Status, protocol, direction, file index and count, file size and bytes done, batch size and bytes done, resume offset, retries, current file name, and the outcome message. While the status is XferAskingE, askLocalSize, askRemoteSize and askCanResume describe the question. + XferProgressT Status, protocol, direction, file index and count, file size and bytes done, batch size and bytes done, resume offset, retries, current file name, and the outcome message. While the status is XferAskingE, askLocalSize, askRemoteSize, and askCanResume describe the question. XferDetectT State of the ZMODEM auto-start detector (zero it, or call xferDetectReset).

    Enumerations

      Enumeration  Values
    @@ -10406,10 +10852,10 @@ if (xferPoll(x, nowMs()) != XferRunningE) {
       bool xferStartReceive(x, proto, io, opt, target, now)     XMODEM: target is the file to create. YMODEM and ZMODEM: target is the folder for the files.
       XferStatusE xferPoll(x, now)                              Move the transfer along. now is a millisecond clock (wrapping is fine).
       void xferCancel(x)                                        Send the cancel sequence to the other side and end with XferCancelledE.
    -  void xferAnswer(x, action, now)                           Answer XferAskingE: XferExistResumeE, RenameE, OverwriteE or SkipE.
    +  void xferAnswer(x, action, now)                           Answer XferAskingE: XferExistResumeE, RenameE, OverwriteE, or SkipE.
       const XferProgressT *xferProgress(x)                      Progress and outcome.
       bool xferDetectFeed(d, byte)                              Feed received terminal bytes; true when a ZMODEM sender's start frame (ZRQINIT) has arrived, so a terminal can start a ZMODEM receive by itself.
    -  void xferSafeName(name, longNames, out)                   Reduce a sender's file name to a safe one: no path, no drive, no DOS device name (CON, PRN, COM1 and so on get a leading _). With longNames (the receiving disk takes long names, platformLongNames) a long name keeping its case, blanks and dots, where a character a long name cannot hold becomes _ and leading blanks and trailing dots and blanks go (RECEIVED when nothing is left); otherwise an 8.3 name of portable characters in upper case. out holds XFER_NAME_MAX bytes (PLATFORM_LONG_NAME_MAX + 1).
    +  void xferSafeName(name, longNames, out)                   Reduce a sender's file name to a safe one: no path, no drive, no DOS device name (CON, PRN, COM1, and so on get a leading _). With longNames (the receiving disk takes long names, platformLongNames) a long name keeping its case, blanks, and dots, where a character a long name cannot hold becomes _ and leading blanks and trailing dots and blanks go (RECEIVED when nothing is left); otherwise an 8.3 name of portable characters in upper case. out holds XFER_NAME_MAX bytes (PLATFORM_LONG_NAME_MAX + 1).
       uint16_t xferCrc16(data, len)                             CRC-16/XMODEM (polynomial 0x1021).
       uint32_t xferCrc32(data, len)                             CRC-32 (IEEE 802.3), as ZMODEM uses it.
       int32_t xferBasNew(&x)                                    For the BASIC functions of other libraries: a new xfer.bas handle owning a new idle transfer (in x) that belongs to the running program; 0 when no handle is free. The library starts the transfer over its own transport; the program follows it with the Xfer functions (the connection library's ConnSendFiles and ConnReceiveFiles work this way).
    @@ -10424,10 +10870,10 @@ if (xferPoll(x, nowMs()) != XferRunningE) {
     

    YMODEM

    A batch of files, always with CRC and 1K blocks (a tail of at most 128 bytes goes in a 128-byte block). Block 0 carries the file name and size before each file (the long name when it fits in the 128-byte block beside the other fields, else the 8.3 name), and an empty block 0 ends the batch. The receiver refuses the first EOT of each file and accepts the second, and cuts the padding using the size.

    ZMODEM

    -

    Headers are sent in hex (to start and answer) or binary with CRC-16 or CRC-32; data travels in subpackets with a CRC over the data and the frame end. ZDLE escaping protects ZDLE, DLE, XON and XOFF (with and without the high bit) and a CR after '@'; with escapeCtl every control character is escaped (the sender announces it with ZSINIT; the receiver asks for it in ZRINIT). Five CANs in a row cancel.

    +

    Headers are sent in hex (to start and answer) or binary with CRC-16 or CRC-32; data travels in subpackets with a CRC over the data and the frame end. ZDLE escaping protects ZDLE, DLE, XON, and XOFF (with and without the high bit), and a CR after '@'; with escapeCtl every control character is escaped (the sender announces it with ZSINIT; the receiver asks for it in ZRINIT). Five CANs in a row cancel.

    The sender streams: it keeps at most 8 KB unacknowledged, asks for a progress acknowledgement every 4 KB, honours the receiver's buffer size (a receiver without flow control should set rxWindow to what its hardware buffer holds, so a slow frame never loses data), and builds a subpacket only when the transport has sent nearly everything it was given (XferIoT.pending). On an error the receiver sends ZRPOS with the last good position; the sender goes back there and halves its subpacket size (down to 64 bytes), then doubles it again after eight clean subpackets. A sender that gets no acknowledgement for the window goes back to the last acknowledged position. File names (long names as they are) travel in the ZFILE subpacket with the size and the files and bytes left in the batch, which the receiver shows as batch progress. Commands from the other side (ZCOMMAND) are never run.

    Files are at most 2 GB (sizes and positions are 32-bit): a send refuses a larger file, a size offered past 2 GB reads as unknown, and a receive that passes 2 GB stops with "A file larger than 2 GB cannot be transferred". A file already on the disk is never mistaken for a missing one, whatever its size.

    -

    Interoperability: the engines are tested against lrz and lsz (lrzsz 0.12.20) in both directions for every protocol, including escaping, resume and long file names.

    +

    Interoperability: the engines are tested against lrz and lsz (lrzsz 0.12.20) in both directions for every protocol, including escaping, resume, and long file names.

    File Transfers from BASIC

    @@ -10437,7 +10883,7 @@ if (xferPoll(x, nowMs()) != XferRunningE) { -------- ----------- XferSendFile(com, proto, path) Send one file over COM port com (1-4). Returns a handle, or 0. XferReceive(com, proto, target) Receive: XMODEM into the file target; YMODEM and ZMODEM into the folder target. Returns a handle, or 0. - XferPoll(handle) Move the transfer along. Returns XFER_RUNNING, XFER_ASKING, XFER_DONE, XFER_FAILED or XFER_CANCELLED. + XferPoll(handle) Move the transfer along. Returns XFER_RUNNING, XFER_ASKING, XFER_DONE, XFER_FAILED, or XFER_CANCELLED. XferStatus(handle) The same, without moving it along (XFER_IDLE for a bad handle). XferSending(handle), XferProtocol(handle) True for a send; the protocol. XferBytes(handle), XferSize(handle) Bytes of the current file done, and its size (-1 when unknown). @@ -10447,7 +10893,7 @@ if (xferPoll(x, nowMs()) != XferRunningE) { XferResumedAt(handle), XferErrors(handle) Where the current file continued from; retries so far. XferAskSize(handle), XferCanResume(handle) XFER_ASKING: the size of the file already there; True when XFER_EXIST_RESUME can continue it. XferAskRemoteSize(handle) XFER_ASKING: the size the sender offers (XferProgressT.askRemoteSize; -1 when unknown). - XferAnswer handle, action Answer XFER_ASKING with XFER_EXIST_RENAME, _RESUME, _OVERWRITE or _SKIP. + XferAnswer handle, action Answer XFER_ASKING with XFER_EXIST_RENAME, _RESUME, _OVERWRITE, or _SKIP. XferMessage$(handle) How the transfer ended, or why it failed. XferCancel handle Stop a running transfer (the other side is told). XferClose handle Free the handle. @@ -10524,7 +10970,7 @@ End Sub onEvent void (*)(WidgetT *w, const char *name, WgtValueT *args, int32_t argc) A named event the widget fires that has no callback above (ListView ColumnClick, TextArea GutterClick, HelpView LinkClick, an event a BASIC control raises). args may be written back.

    wgtFireEvent(w, name, args, argc) fires an event by name: the names above (Click, DblClick, Change, GotFocus, LostFocus, KeyPress, KeyDown, KeyUp, MouseDown, MouseUp, MouseMove, Scroll) go to their callback, every other name to onEvent (see dvxWgt.h, Custom Events).

    Generic Access

    -

    Every widget type can be created, read, written and called by name through its interface descriptor, with values as WgtValueT -- the way the BASIC form runtime and the designer drive widgets, and the way a C program uses a widget it has no header for (a BASIC control, for one):

    +

    Every widget type can be created, read, written, and called by name through its interface descriptor, with values as WgtValueT -- the way the BASIC form runtime and the designer drive widgets, and the way a C program uses a widget it has no header for (a BASIC control, for one):

    WidgetT  *list = wgtCreateByName("ListBox", parent);
     WgtValueT item = wgtValueStr("first");
     WgtValueT count;
    @@ -10553,7 +10999,8 @@ wgtCallMethod(list, "ListCount", NULL, 0, &count);   // count.i ==
     

    AnsiTerm

    -

    A VT100/ANSI-compatible terminal emulator widget designed for connecting to BBS systems over the serial link. Uses a traditional text-mode cell buffer (character + attribute byte pairs) with the CP437 character set and 16-color CGA palette. Supports cursor movement, screen/line erase, scrolling regions, SGR colors, DEC private modes, and a configurable scrollback buffer. Communication is abstracted through read/write function pointers, allowing the terminal to work with raw serial ports, the secLink encrypted channel, or any other byte-oriented transport.

    +

    A VT100/ANSI-compatible terminal emulator widget designed for connecting to BBS systems over the serial link. Uses a traditional text-mode cell buffer (character + attribute byte pairs) with the CP437 character set and 16-color CGA palette. Supports cursor movement, screen/line erase, scrolling regions, SGR colors, DEC private modes, ANSI music, and a configurable scrollback buffer. Communication is abstracted through read/write function pointers, allowing the terminal to work with raw serial ports, the secLink encrypted channel, or any other byte-oriented transport.

    +

    WCLASS_FIXED_FONT: it edits by columns, so its font must be fixed-pitch. Any size works (the column math uses the font's charWidth and charHeight); a proportional font set with wgtSetFont, or inherited, is replaced by the system font when it draws.

    The widget renders through two paint paths: a full paint used by the normal widget pipeline, and a fast incremental repaint (wgtAnsiTermRepaint) that pushes dirty rows directly to the window content buffer for low-latency serial echo.

    Header: widgets/ansiTerm.h

    Creation

    @@ -10586,11 +11033,13 @@ wgtCallMethod(list, "ListCount", NULL, 0, &count); // count.i == ---- -------- ----------- header WGT_API_HEADER ABI version and size, checked by wgtGetApi. copySelection bool wgtAnsiTermCopy(w) Copy the selection to the clipboard and clear it (a menu's Copy). Returns false when nothing is selected. - setPalette void wgtAnsiTermSetPalette(w, rgb) Replace the 16 attribute colors with ANSITERM_PALETTE_BYTES bytes of red, green, blue triplets (a color scheme). NULL restores the standard CGA colors. + setPalette void wgtAnsiTermSetPalette(w, rgb) Replace the 16 attribute colors with ANSITERM_PALETTE_BYTES bytes of red, green, blue triplets (a color scheme). NULL restores the standard CGA colors. + setMusic void wgtAnsiTermSetMusic(w, on) Recognize ANSI music (on at first). Off, ESC[M is always Delete Line and ESC[N and ESC[| do nothing.

    Properties (BASIC Interface)

      Property     Type       Access       Description
       --------     ----       ------       -----------
    -  Cols         Integer    Read/Write   Number of columns. Setting it (or Rows) resizes the terminal (1-512): screen and scrollback start empty and the modes the host set (wrap, origin, cursor visible, scroll region) are reset; palette, scrollback length and comm callbacks stay; the window is laid out again.
    +  Cols         Integer    Read/Write   Number of columns. Setting it (or Rows) resizes the terminal (1-512): screen and scrollback start empty and the modes the host set (wrap, origin, cursor visible, scroll region) are reset; palette, scrollback length, and comm callbacks stay; the window is laid out again.
    +  Music        Boolean    Read/Write   Recognize ANSI music (setMusic; True at first).
       Rows         Integer    Read/Write   Number of rows.
       Scrollback   Integer    Read/Write   Maximum number of scrollback lines. Setting it discards the existing history.

    Methods (BASIC Interface)

    @@ -10604,7 +11053,12 @@ wgtCallMethod(list, "ListCount", NULL, 0, &count); // count.i == SetColor index, color: one attribute color (0-15) from a BASIC color (&HBBGGRR, converted with wgtBasicColor). Write Write a string into the terminal (with ANSI escape processing).

    Events

    -

    AnsiTerm uses the common events only. No widget-specific events are defined.

    +

    Besides the common events, two of its own, fired through wgtFireEvent (a C host gets them in onEvent with the names ANSITERM_EVT_MUSIC and ANSITERM_EVT_BELL):

    +
      Event   Parameters          Description
    +  -----   ----------          -----------
    +  Music   Macro As String     An ANSI music string arrived: the BASIC PLAY text between ESC[| or ESC[N, or ESC[MF (MB, MN, ML, MS), and Ctrl+N (0x0E). For the ESC[M form the macro begins with that M command (e.g. "MFT120O4L8CDE"). Line ends are left out; a string longer than 4096 characters, an empty one, or one cut short by ESC fires nothing.
    +  Bell    (none)              A BEL character (Ctrl+G) arrived.
    +

    The widget plays nothing itself: a handler plays the macro (BASIC: PLAY Macro; C: the music library's musicMmlPlay). While a Music or Bell handler runs, the terminal takes no input: Poll leaves the bytes in the connection, and Write keeps them, in order, until the handler returns. A tune played in the foreground (MF) therefore holds back the text after it, as on a DOS terminal, while the window stays responsive. Bare ESC[M (no parameter) followed by anything but F, B, N, L, or S is still Delete Line. A handler may close the terminal's window; the input after the event is then dropped.

    Keyboard Shortcuts

    Within the terminal widget:

      Key              Action
    @@ -10616,7 +11070,7 @@ wgtCallMethod(list, "ListCount", NULL, 0, &count);   // count.i ==
       PgUp/PgDn/Del    Send the corresponding VT100 escape sequences.
       Shift+PgUp       Page back through the scrollback history.
       Shift+PgDn       Page forward through the scrollback history.
    -

    Any key sent to the host returns the view to the live screen. The mouse wheel scrolls through the scrollback history locally; it does not send keys to the host. Clearing the screen (ESC[2J, form feed, reset or Clear) saves the screen into the scrollback only up to its last non-blank row.

    +

    Any key sent to the host returns the view to the live screen. The mouse wheel scrolls through the scrollback history locally; it does not send keys to the host. Clearing the screen (ESC[2J, form feed, reset, or Clear) saves the screen into the scrollback only up to its last non-blank row.

    Box (VBox / HBox / Frame)

    @@ -10713,7 +11167,7 @@ btn->onClick = onOkClicked;
    void wgtCanvasResize(w, newW, newH) Resize the canvas buffer and relayout. The old content is discarded; the new buffer is white. void wgtCanvasSetBorderStyle(w, style) CANVAS_BORDER_NONE (0) drops the sunken frame, any other value (CANVAS_BORDER_FIXED, 1, the default) shows it. The buffer keeps its size; the widget relays out. int32_t wgtCanvasGetBorderStyle(w) CANVAS_BORDER_FIXED or CANVAS_BORDER_NONE. - bool wgtCanvasGetDisplay(w, out) Fill a DisplayT aimed at the canvas pixels (the screen's pixel format, clipped to the canvas), so any libdvx drawing call -- drawText, drawBevel, drawArrowGlyph, wmDrawScrollbarAt, rectCopyTransparent -- draws on the canvas with the context's blit ops; call wgtInvalidatePaint afterwards. False (out untouched) for a widget that is not a canvas. The BASIC DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow and PrintAccel methods use it. + bool wgtCanvasGetDisplay(w, out) Fill a DisplayT aimed at the canvas pixels (the screen's pixel format, clipped to the canvas), so any libdvx drawing call -- drawText, drawBevel, drawArrowGlyph, wmDrawScrollbarAt, rectCopyTransparent -- draws on the canvas with the context's blit ops; call wgtInvalidatePaint afterwards. False (out untouched) for a widget that is not a canvas. The BASIC DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, and PrintAccel methods use it.

    API Struct (wgtRegisterApi "canvas")

      Slot                 Function
       ----                 --------
    @@ -10753,8 +11207,8 @@ btn->onClick = onOkClicked;
    DrawText x%, y%, text$ Draw text at the given position. FillCircle cx%, cy%, radius%, color% Fill a circle. FillRect x%, y%, w%, h%, color% Fill a rectangle. - GetPixel(x%, y%) Returns the color at a pixel (&HBBGGRR). With fewer than 16 million colors on the screen it is the color the screen shows: black, white and full colors such as RGB(255, 0, 0) read back exactly, others may differ slightly. - Load(path$) Load a BMP, PNG, JPEG or GIF image (the canvas takes its size). Returns 0 on success, -1 on error (WGT_SIG_RET_INT_STR). + GetPixel(x%, y%) Returns the color at a pixel (&HBBGGRR). With fewer than 16 million colors on the screen it is the color the screen shows: black, white, and full colors such as RGB(255, 0, 0) read back exactly, others may differ slightly. + Load(path$) Load a BMP, PNG, JPEG, or GIF image (the canvas takes its size). Returns 0 on success, -1 on error (WGT_SIG_RET_INT_STR). Resize w%, h% Resize the canvas (the drawing is cleared to white). Save(path$) Save the canvas: BMP when path$ ends in .bmp, PNG otherwise. Returns 0 on success, -1 on error (WGT_SIG_RET_INT_STR). SetPenColor color% Set the drawing color (&HBBGGRR). @@ -10805,9 +11259,10 @@ btn->onClick = onOkClicked;

    ComboBox

    -

    A combination of a single-line text input and a drop-down list. The user can type a value or select one from the list. When the user picks a list item, its text is copied into the edit buffer. Supports full text editing (cursor movement, selection, clipboard, undo) via the texthelp library and a popup overlay list via the listhelp library. Setting the text or picking a list item clears the undo history. Keyboard: Down, F4 or Alt+Down/Alt+Up opens the list; while it is open Down/Up move the highlight and Enter, F4 or Alt+Down/Alt+Up pick the item and close it (Esc closes without picking).

    -

    A read-only combo box (wgtSetReadOnly) ignores the editing keys (typing, Backspace, Delete, paste, cut, undo) but still allows cursor movement, selection, copy and picking an item from the list.

    +

    A combination of a single-line text input and a drop-down list. The user can type a value or select one from the list. When the user picks a list item, its text is copied into the edit buffer. Supports full text editing (cursor movement, selection, clipboard, undo) via the texthelp library and a popup overlay list via the listhelp library. Setting the text or picking a list item clears the undo history. Keyboard: Down, F4, or Alt+Down/Alt+Up opens the list; while it is open Down/Up move the highlight and Enter, F4, or Alt+Down/Alt+Up pick the item and close it (Esc closes without picking).

    +

    A read-only combo box (wgtSetReadOnly) ignores the editing keys (typing, Backspace, Delete, paste, cut, undo) but still allows cursor movement, selection, copy, and picking an item from the list.

    Depends on "texthelp" and "listhelp" helper libraries (declared in combobox.dep).

    +

    WCLASS_FIXED_FONT: it edits by columns, so its font must be fixed-pitch. Any size works (the column math uses the font's charWidth and charHeight); a proportional font set with wgtSetFont, or inherited, is replaced by the system font when it draws.

    Header: widgets/comboBox.h

    Creation

    WidgetT *cb = wgtComboBox(parent, 128);
    @@ -10946,7 +11401,7 @@ wgtComboBoxAddItem(cb, "Times");
    ----- ----------- Reposition Fires when the current row changes. Default event. Validate Fires before writing. Return false to cancel the write. -

    While the Validate handler runs, the moves (MoveFirst/MoveLast/MoveNext/MovePrevious, dataCtrlSetCurrentRow), AddNew, Refresh and Update on the same control are refused and return without doing anything: each would save, and therefore validate, the same row again. Delete is still allowed; Update re-checks the row after the handler returns.

    +

    While the Validate handler runs, the moves (MoveFirst/MoveLast/MoveNext/MovePrevious, dataCtrlSetCurrentRow), AddNew, Refresh, and Update on the same control are refused and return without doing anything: each would save, and therefore validate, the same row again. Delete is still allowed; Update re-checks the row after the handler returns.

    DbGrid

    @@ -10998,7 +11453,7 @@ dbGridSetDataWidget(grid, dataCtrl);

    Dropdown

    -

    A drop-down list that displays a single selected item and expands to a popup list when clicked. Unlike ComboBox, the user cannot type free text -- only list items can be selected. Keyboard: while closed, Down/Up change the selection and type-ahead picks the next item starting with a printable key; Space, Enter, F4 or Alt+Down/Alt+Up open the list. While open, Down/Up move the highlight and Enter, Space, F4 or Alt+Down/Alt+Up select it and close the list (Esc closes without selecting). Ctrl and Alt chords never change the selection. The popup overlay is rendered above all other widgets via the widget paint overlay mechanism.

    +

    A drop-down list that displays a single selected item and expands to a popup list when clicked. Unlike ComboBox, the user cannot type free text -- only list items can be selected. Keyboard: while closed, Down/Up change the selection and type-ahead picks the next item starting with a printable key; Space, Enter, F4, or Alt+Down/Alt+Up open the list. While open, Down/Up move the highlight and Enter, Space, F4, or Alt+Down/Alt+Up select it and close the list (Esc closes without selecting). Ctrl and Alt chords never change the selection. The popup overlay is rendered above all other widgets via the widget paint overlay mechanism.

    Depends on the "listhelp" helper library (declared in dropdown.dep).

    Header: widgets/dropdown.h

    Creation

    @@ -11052,8 +11507,8 @@ wgtDropdownAddItem(dd, "Blue");

    HelpView

    -

    One topic of a compiled help file, shown the way the DVX Help Viewer shows it -- the Help Viewer itself uses this widget. Headings (three levels), word-wrapped paragraphs and list items, notes (info, tip and warning), code blocks and tables (preformatted), pictures (left, centered or right), horizontal rules and links, in a scrolling pane, in the theme's colours. Text re-wraps when the widget changes width.

    -

    Links take the keyboard focus (Tab) and are followed with a click, Enter or Space: the widget keeps the link's target topic ID (wgtHelpViewGetLinkTarget) and calls the link callback with it. The widget does not change topic by itself; the callback usually finds the topic (hlpFileFindTopic) and shows it, which may rebuild the pane from inside the link's own handler. Everything a topic shows is copied when it is shown, so the help file may be closed afterwards.

    +

    One topic of a compiled help file, shown the way the DVX Help Viewer shows it -- the Help Viewer itself uses this widget. Headings (three levels), word-wrapped paragraphs and list items, notes (info, tip, and warning), code blocks and tables (preformatted), pictures (left, centered, or right), horizontal rules and links, in a scrolling pane, in the theme's colours. Text re-wraps when the widget changes width.

    +

    Links take the keyboard focus (Tab) and are followed with a click, Enter, or Space: the widget keeps the link's target topic ID (wgtHelpViewGetLinkTarget) and calls the link callback with it. The widget does not change topic by itself; the callback usually finds the topic (hlpFileFindTopic) and shows it, which may rebuild the pane from inside the link's own handler. Everything a topic shows is copied when it is shown, so the help file may be closed afterwards.

    The help file is read with the help file library (hlpfile/hlpFile.h).

    Header: widgets/helpView.h

    BASIC name: HelpPane (see the HelpPane control in the BASIC reference)

    @@ -11268,7 +11723,7 @@ wgtListBoxAddItem(lb, "Gamma");

    ListView

    -

    A multi-column list with sortable headers. Supports single and multi-selection, column alignment, header click sorting, column resizing (drag a header border; double-click it to fit the contents), and drag-to-reorder. Data is provided as a flat array of strings (row-major order, one string per cell). wgtListViewSetData keeps a pointer to the caller's array; AddItem, RemoveRow, Clear, SetCell and drag-to-reorder work on a copy the widget makes first, so the caller's array is never modified.

    +

    A multi-column list with sortable headers. Supports single and multi-selection, column alignment, header click sorting, column resizing (drag a header border; double-click it to fit the contents), and drag-to-reorder. Data is provided as a flat array of strings (row-major order, one string per cell). wgtListViewSetData keeps a pointer to the caller's array; AddItem, RemoveRow, Clear, SetCell, and drag-to-reorder work on a copy the widget makes first, so the caller's array is never modified.

    Sorting and drag-to-reorder are exclusive: enabling reorder clears the sort, and while reorder is enabled wgtListViewSetSort and header clicks do not sort. A column width set by dragging or double-clicking a header border is kept when the data changes, until new columns are set.

    Header: widgets/listView.h

    Creation

    @@ -11426,9 +11881,111 @@ WidgetT *r2 = wgtRadio(grp, "Option B");

    Default Event

    "Click" (VB basName: OptionButton, namePrefix: Option).

    +
    +

    RichEdit

    +

    A text box for styled text: runs in fonts, sizes, styles, and colors, in paragraphs aligned left, centered, or right, edited with the keyboard and the mouse, with undo and redo, a clipboard that keeps styles between RichEdit controls, find, and RTF in and out. The document is a RichTextT of the richtext library (richtext/richText.h); a style's family "" and size 0 are the widget's own font (wgtSetFont, or its container's) and RICHEDIT_COLOR_DEFAULT its fgColor, so such text follows the widget.

    +

    Each paragraph is laid out by a text flow of its own (dvxFlow.h): an edit lays out only the paragraphs it touched, and the paragraphs' tops are kept for binary searches from an offset or a row. The widget polls (WCLASS_NEEDS_POLL) and has a quick repaint (WGT_METHOD_QUICK_REPAINT): an edit that left every paragraph's height alone, a caret or selection move, and the caret's blink repaint only the rows they touched, painted straight into the window's content buffer (widgetQuickPaintBegin); a change of height, a scroll, or a new width repaints the widget. Undo keeps for each edit what it replaced and the length it put in its place (alignment changes keep the old alignments); typed characters join one step; 100 steps are kept.

    +

    Depends on the "texthelp" (word boundaries, the caret blink time, scroll bar drags) and "richtext" libraries (declared in richedit.dep).

    +

    Header: widgets/richEdit.h

    +

    Creation

    +
    WidgetT *re = wgtRichEdit(parent);
    +
    +wgtSetText(re, "Dear friend,");
    +wgtRichEditSetSelStart(re, 0);
    +wgtRichEditSetSelLength(re, 4);
    +wgtRichEditSetSelStyle(re, FONT_STYLE_BOLD, FONT_STYLE_BOLD);
    +wgtRichEditSetSelColor(re, DVX_RGB(0, 0, 160));
    +

    API Functions

    +
      Function                                              Description
    +  --------                                              -----------
    +  WidgetT *wgtRichEdit(parent)                          Create an empty rich edit box.
    +  const char *wgtRichEditGetRtf(w)                      The whole text as RTF (valid until the next call that returns RTF).
    +  void wgtRichEditSetRtf(w, rtf)                        Replace the whole text from RTF (plain text when it is not RTF); undo is emptied.
    +  const char *wgtRichEditGetSelRtf(w)                   The selection as RTF.
    +  void wgtRichEditSetSelRtf(w, rtf)                     Replace the selection from RTF.
    +  int32_t wgtRichEditGetSelStart(w) / SetSelStart       Where the selection starts; setting it places the caret.
    +  int32_t wgtRichEditGetSelLength(w) / SetSelLength     The selection's length; setting it selects from SelStart.
    +  const char *wgtRichEditGetSelText(w) / SetSelText     The selection's text; setting it replaces the selection with plain text.
    +  const char *wgtRichEditGetSelFontName(w)              The selection's family (the widget's own when it uses that).
    +  int32_t wgtRichEditGetSelFontSize(w)                  The selection's pixel size (as opened: the nearest the family has).
    +  void wgtRichEditSetSelFont(w, family, size)           The family (NULL: unchanged; "": the widget's) and size (0: unchanged).
    +  uint32_t wgtRichEditGetSelStyle(w)                    The selection's FONT_STYLE_* bits.
    +  void wgtRichEditSetSelStyle(w, style, mask)           Make the bits in mask style's.
    +  uint32_t wgtRichEditGetSelColor(w) / SetSelColor      0x00RRGGBB or RICHEDIT_COLOR_DEFAULT.
    +  int32_t wgtRichEditGetSelAlign(w) / SetSelAlign       FLOW_ALIGN_* of the selection's paragraphs.
    +  bool wgtRichEditCanUndo(w) / CanRedo                  Whether there is a step to undo or redo.
    +  void wgtRichEditUndo(w) / Redo                        Undo or redo a step.
    +  void wgtRichEditCut(w) / Copy / Paste                 The clipboard (plain text there; the styled copy kept for a paste of the same text).
    +  void wgtRichEditSelectAll(w)                          Select everything.
    +  int32_t wgtRichEditFind(w, text, matchCase)           Select the next match from the selection's end, around past the end; its offset or -1.
    +  int32_t wgtRichEditLoadFile(w, path)                  Load RTF or plain text; 0 or -1.
    +  int32_t wgtRichEditSaveFile(w, path)                  Save RTF (path ending in .rtf) or plain text; 0 or -1.
    +

    The style and alignment setters change the selection, or with none, the style typing uses next (alignment: the caret's paragraph); each change is an undo step and fires Change. wgtGetText / wgtSetText are the plain text, a line per paragraph. RICHEDIT_EVT_SELCHANGE ("SelChange") is fired with wgtFireEvent when the selection or the caret moves.

    +

    API Struct (wgtRegisterApi "richedit")

    +

    The slots are the functions above in order: create, getRtf, setRtf, getSelRtf, setSelRtf, getSelStart, setSelStart, getSelLength, setSelLength, getSelText, setSelText, getSelFontName, getSelFontSize, setSelFont, getSelStyle, setSelStyle, getSelColor, setSelColor, getSelAlign, setSelAlign, canUndo, canRedo, undo, redo, cut, copy, paste, selectAll, find, loadFile, and saveFile.

    +

    BASIC Interface

    +

    Properties: TextRTF and SelRTF (wgtRichEditGetRtf / SetRtf, GetSelRtf / SetSelRtf), SelStart, SelLength, SelText, SelBold, SelItalic, SelUnderline, SelStrikethru (one FONT_STYLE_* bit of SetSelStyle each), SelColor (a BASIC color; -1 for ForeColor), SelFontName and SelFontSize (wgtRichEditSetSelFont with the other part left alone), SelAlignment, and the read-only CanUndo and CanRedo. Methods: Undo, Redo, Cut, Copy, Paste, SelectAll, Find (any case), LoadFile, and SaveFile. Event: SelChange; default event Change.

    +

    Methods Used

    +

    PAINT, CALC_MIN_SIZE (20 by 4 characters of its font, with border and scroll bar), ON_MOUSE, ON_KEY, ON_DRAG_UPDATE, ON_DRAG_END, ON_WHEEL, ON_FOCUS, ON_BLUR, POLL, QUICK_REPAINT, GET_TEXT, SET_TEXT, CLEAR_SELECTION, DESTROY. Flags: WCLASS_FOCUSABLE, WCLASS_NEEDS_POLL.

    +
    +
    +

    RichLabel

    +

    Text in many fonts, styles, and colors, wrapped and aligned paragraph by paragraph. The text is built by calls: wgtRichLabelSetFont, wgtRichLabelSetStyle, and wgtRichLabelSetColor choose what the following wgtRichLabelAddText uses; wgtRichLabelAddLineBreak starts a new line and wgtRichLabelAddParagraph a new paragraph with its own alignment. Text starts in the widget's own font (wgtSetFont, or its container's). The widget is laid out at its width hint (prefW, else minW, which BASIC's Width sets), wrapping at spaces (a word too wide for a line breaks inside it); without one each paragraph is one line. Every line is as tall as the tallest font on it, its text standing on one baseline. Not focusable; drawn the same enabled or not. It fills its rectangle as a Label does: with the nearest explicit bgColor, else the content background. Built on the text flow in libdvx (dvxFlow.h).

    +

    Header: widgets/richLbl.h

    +

    Creation

    +
    WidgetT *rl = wgtRichLabel(parent);
    +
    +wgtRichLabelSetFont(rl, "DejaVu Serif", 18);
    +wgtRichLabelSetStyle(rl, FONT_STYLE_BOLD);
    +wgtRichLabelAddText(rl, "Chapter One");
    +wgtRichLabelAddParagraph(rl, FLOW_ALIGN_LEFT);
    +wgtRichLabelSetFont(rl, "", 0);
    +wgtRichLabelSetStyle(rl, 0);
    +wgtRichLabelAddText(rl, "It was a ");
    +wgtRichLabelSetColor(rl, DVX_RGB(160, 0, 0));
    +wgtRichLabelAddText(rl, "dark");
    +wgtRichLabelSetColor(rl, RICHLABEL_COLOR_DEFAULT);
    +wgtRichLabelAddText(rl, " and stormy night.");
    +

    API Functions

    +
      Function                                       Description
    +  --------                                       -----------
    +  WidgetT *wgtRichLabel(parent)                  Create an empty rich label.
    +  void wgtRichLabelClear(w)                      Remove all text; the font, style, and color chosen go back to the widget's.
    +  void wgtRichLabelSetFont(w, family, size)      The font family and pixel size of the text added next ("" and 0: the widget's own).
    +  void wgtRichLabelSetStyle(w, style)            FONT_STYLE_BOLD, ITALIC, UNDERLINE, and STRIKE bits for the text added next.
    +  void wgtRichLabelSetColor(w, rgb)              0x00RRGGBB color of the text added next; RICHLABEL_COLOR_DEFAULT: the widget's fgColor (else the scheme's).
    +  void wgtRichLabelAddText(w, text)              Add text in the font, style, and color chosen.
    +  void wgtRichLabelAddLineBreak(w)               Start a new line in the same paragraph.
    +  void wgtRichLabelAddParagraph(w, align)        Start a new paragraph aligned FLOW_ALIGN_LEFT (0), FLOW_ALIGN_CENTER (1), or FLOW_ALIGN_RIGHT (2), as a Label's alignment; others are ignored.
    +

    Setting the font, style, or color changes only the text added afterwards.

    +

    API Struct (wgtRegisterApi "richlabel")

    +
      Slot           Function
    +  ----           --------
    +  header         WGT_API_HEADER (ABI version and size, checked by wgtGetApi)
    +  create         wgtRichLabel
    +  clear          wgtRichLabelClear
    +  setFont        wgtRichLabelSetFont
    +  setStyle       wgtRichLabelSetStyle
    +  setColor       wgtRichLabelSetColor
    +  addText        wgtRichLabelAddText
    +  addLineBreak   wgtRichLabelAddLineBreak
    +  addParagraph   wgtRichLabelAddParagraph
    +

    Methods (BASIC Interface)

    +
      Method         Arguments              Description
    +  ------         ---------              -----------
    +  Clear          (none)                 wgtRichLabelClear
    +  SetFont        family, size           wgtRichLabelSetFont
    +  SetStyle       style                  wgtRichLabelSetStyle
    +  SetColor       color                  A BASIC color (RGB()); -1 for ForeColor
    +  AddText        text                   wgtRichLabelAddText
    +  AddLineBreak   (none)                 wgtRichLabelAddLineBreak
    +  AddParagraph   alignment              wgtRichLabelAddParagraph
    +

    Default Event

    +

    "Click" (VB basName: RichLabel).

    +

    ScrollPane

    -

    A scrollable container that provides vertical and/or horizontal scrollbars when its content exceeds the visible area. Place a single child (typically a VBox or HBox) inside the scroll pane. A click on content that does not handle the mouse itself (a label, an image or empty space) gives the scroll pane the keyboard focus, so the arrow and page keys scroll it.

    +

    A scrollable container that provides vertical and/or horizontal scrollbars when its content exceeds the visible area. Place a single child (typically a VBox or HBox) inside the scroll pane. A click on content that does not handle the mouse itself (a label, an image, or empty space) gives the scroll pane the keyboard focus, so the arrow and page keys scroll it.

    Header: widgets/scrlPane.h

    Creation

    WidgetT *sp = wgtScrollPane(parent);
    @@ -11446,7 +12003,7 @@ WidgetT *content = wgtVBox(sp);
       int32_t wgtScrollPaneGetScrollLeft(w)             Horizontal scroll position in pixels.
       void wgtScrollPaneSetScrollLeft(w, pos)           Set the horizontal scroll position; the layout clamps it to the content.

    Padding and spacing are the common WidgetT padding / spacing fields (WGT_GAP_DEFAULT, the default, is none); wgtSetPaddingPx / wgtSetSpacingPx set them in pixels.

    -

    When the user moves the scroll position (scrollbar arrows and trough, dragging a thumb, the arrow, page, Home and End keys) the scroll pane fires onChange (widgetFireChange) after the position is clamped; the setters, wgtScrollPaneScrollToTop and scrolling a focused child into view do not. BASIC sees it as the ScrollPane's Change event.

    +

    When the user moves the scroll position (scrollbar arrows and trough, dragging a thumb, the arrow, page, Home, and End keys) the scroll pane fires onChange (widgetFireChange) after the position is clamped; the setters, wgtScrollPaneScrollToTop, and scrolling a focused child into view do not. BASIC sees it as the ScrollPane's Change event.

    API Struct (wgtRegisterApi "scrollpane")

      Slot             Function
       ----             --------
    @@ -11535,7 +12092,7 @@ WidgetT *content = wgtVBox(sp);
     

    Events

      Callback     Description
       --------     -----------
    -  onChange     Fires when the slider value changes (not when a key, click or drag leaves it unchanged).
    + onChange Fires when the slider value changes (not when a key, click, or drag leaves it unchanged).

    Default Event

    "Change" (VB basName: HScrollBar, namePrefix: HScroll).

    @@ -11565,7 +12122,8 @@ wgtButton(row, "Cancel");

    Spinner

    A numeric input with up/down buttons for incrementing and decrementing a value within a range. Supports both integer and floating-point (real) modes.

    -

    Typing, Backspace/Delete, paste, cut and undo start an edit of the text; Enter or focus loss commits it and Esc cancels it. Undo (Ctrl+Z) covers edits of the typed text only: every reformat from the value (creation, SetText, commit, step) empties the undo slot. A paste that is not a valid number is rolled back.

    +

    Typing, Backspace/Delete, paste, cut, and undo start an edit of the text; Enter or focus loss commits it and Esc cancels it. Undo (Ctrl+Z) covers edits of the typed text only: every reformat from the value (creation, SetText, commit, step) empties the undo slot. A paste that is not a valid number is rolled back.

    +

    WCLASS_FIXED_FONT: it edits by columns, so its font must be fixed-pitch. Any size works (the column math uses the font's charWidth and charHeight); a proportional font set with wgtSetFont, or inherited, is replaced by the system font when it draws.

    Header: widgets/spinner.h

    Creation

    WidgetT *sp = wgtSpinner(parent, 0, 100, 1);
    @@ -11711,13 +12269,14 @@ WidgetT *page2 = wgtTabPage(tabs, "Advanced");

    Events

      Callback     Description
       --------     -----------
    -  onChange     Fires when the user changes the active tab (mouse, keyboard or accelerator); not for wgtTabControlSetActive.
    + onChange Fires when the user changes the active tab (mouse, keyboard, or accelerator); not for wgtTabControlSetActive.

    Default Event

    "Click" (VB basName: TabStrip).

    TextInput / TextArea

    -

    Single-line text input, password input, masked input, and multi-line text area with optional syntax colorization (built-in BASIC and help-source colorizers, the BASIC one shared with the DVX BASIC IDE), line numbers, find/replace, line markers and gutter clicks.

    +

    Single-line text input, password input, masked input, and multi-line text area with optional syntax colorization (built-in BASIC and help-source colorizers, the BASIC one shared with the DVX BASIC IDE), line numbers, find/replace, line markers, and gutter clicks.

    +

    WCLASS_FIXED_FONT: it edits by columns, so its font must be fixed-pitch. Any size works (the column math uses the font's charWidth and charHeight); a proportional font set with wgtSetFont, or inherited, is replaced by the system font when it draws.

    Header: widgets/textInpt.h

    Creation

      Macro                            Description
    @@ -11742,7 +12301,7 @@ WidgetT *page2 = wgtTabPage(tabs, "Advanced");
       bool wgtTextAreaReplaceSelection(w, needle, repl, caseSens, fwd)  If the current selection matches needle, replace it, then find the next occurrence in direction fwd. Returns true if anything was replaced or found.
       void wgtTextAreaSetLineMarker(w, line, gutterRgb, backRgb)  Mark 1-based line: a dot of gutterRgb (0xRRGGBB) in the line-number gutter and / or the line's background in backRgb; TEXTAREA_MARKER_NONE leaves a part out, both NONE removes the marker. Markers keep their line numbers when the text changes (the owner moves them). The DVX BASIC IDE marks breakpoints and the paused line this way.
       void wgtTextAreaClearLineMarkers(w)                         Remove every line marker.
    -  void wgtTextAreaSetSyntaxMode(w, mode)                      Use a built-in colorizer: "bas" / "basic" (BASIC: the compiler's keyword list, date literals by the lexer's shape rule, &H / &O and exponent numbers, REM and ' comments; the IDE's code editor uses it), "dhs" / "help" (help source) or "custom" (each painted line goes to the "Colorize" event through wgtFireEvent: args LineText and Colors, a string of one digit per character, "0" = TEXT_SYNTAX_DEFAULT; the host writes the TEXT_SYNTAX_* digits back into Colors; the BASIC Colorize event); NULL or "" turns colouring off.
    +  void wgtTextAreaSetSyntaxMode(w, mode)                      Use a built-in colorizer: "bas" / "basic" (BASIC: the compiler's keyword list, date literals by the lexer's shape rule, &H / &O and exponent numbers, REM and ' comments; the IDE's code editor uses it), "dhs" / "help" (help source), or "custom" (each painted line goes to the "Colorize" event through wgtFireEvent: args LineText and Colors, a string of one digit per character, "0" = TEXT_SYNTAX_DEFAULT; the host writes the TEXT_SYNTAX_* digits back into Colors; the BASIC Colorize event); NULL or "" turns colouring off.
       int32_t wgtTextAreaGetCursorLine(w)                         Get the current cursor line number.
       void wgtTextAreaSetGutterClickCallback(w, fn)               Set a callback for gutter clicks (e.g. for breakpoint toggling).
       int32_t wgtTextAreaGetWordAtCursor(w, buf, bufSize)         Copy the word under the cursor into buf. Returns its length.
    @@ -11755,7 +12314,7 @@ WidgetT *page2 = wgtTabPage(tabs, "Advanced");
       void wgtTextInputSetSelStart(w, start)                      Cursor to offset start, nothing selected.
       void wgtTextInputSetSelLength(w, length)                    Select length characters from SelStart (clamped to the text).
       void wgtTextInputSetSelText(w, text)                        Replace the selection (or insert at the cursor) with text (texthelp textEditSingleReplaceSel; a masked box fills its slots); fires onChange.
    -  void wgtTextInputCut(w), wgtTextInputCopy(w), wgtTextInputPaste(w), wgtTextInputSelectAll(w), wgtTextInputUndo(w)   The box's Ctrl+X / C / V / A / Z keys, run through its own key handler (password, mask and read-only rules included).
    + void wgtTextInputCut(w), wgtTextInputCopy(w), wgtTextInputPaste(w), wgtTextInputSelectAll(w), wgtTextInputUndo(w) The box's Ctrl+X / C / V / A / Z keys, run through its own key handler (password, mask, and read-only rules included).

    API Struct (wgtRegisterApi "textinput")

    The combined "textinput" API exposes all four constructors plus the TextArea functions and, appended at its end (inputGetSelStart ... inputUndo), the TextBox selection and clipboard functions. The designer uses per-type APIs registered under the names "textbox" (single-line) and "textarea" (multi-line), each exposing only the header and a create slot. Every API struct starts with a header (WGT_API_HEADER: ABI version and size, checked by wgtGetApi).

    Properties (BASIC Interface)

    @@ -11768,7 +12327,7 @@ WidgetT *page2 = wgtTabPage(tabs, "Advanced"); SelStart Integer Read/Write wgtTextInputGetSelStart / wgtTextInputSetSelStart. SelLength Integer Read/Write wgtTextInputGetSelLength / wgtTextInputSetSelLength. SelText String Read/Write wgtTextInputGetSelText / wgtTextInputSetSelText. -

    TextBox methods: Copy, Cut, Paste, SelectAll and Undo (wgtTextInputCopy ...).

    +

    TextBox methods: Copy, Cut, Paste, SelectAll, and Undo (wgtTextInputCopy ...).

    TextArea exposes:

      Property     Type       Access       Description
       --------     ----       ------       -----------
    @@ -11784,7 +12343,7 @@ WidgetT *page2 = wgtTabPage(tabs, "Advanced");
       ------                                            -----------
       AppendText text$                                  Append text to the end of the buffer.
       ClearLineMarkers                                  wgtTextAreaClearLineMarkers.
    -  Copy / Cut / Paste / SelectAll / Undo             The Ctrl+C / X / V / A / Z commands (the texthelp textEditMulti* functions the key handler runs); Cut, Paste and Undo fire Change and do nothing when read-only.
    +  Copy / Cut / Paste / SelectAll / Undo             The Ctrl+C / X / V / A / Z commands (the texthelp textEditMulti* functions the key handler runs); Cut, Paste, and Undo fire Change and do nothing when read-only.
       CursorToEnd                                       wgtTextAreaCursorToEnd.
       FindNext needle$, caseSensitive, forward          Search from the cursor (a match at the cursor counts; repeating moves past it). Returns True if found.
       GetLine(line%)                                    Returns a line's text (1-based), "" past the end.
    diff --git a/installToolchain.sh b/installToolchain.sh
    index 9e33afd2..79dd4385 100755
    --- a/installToolchain.sh
    +++ b/installToolchain.sh
    @@ -31,6 +31,8 @@
     #                        linux64 release of andrewwutw/build-djgpp v3.4)
     #   toolchains/cwsdpmi   CWSDPMI r7 (csdpmi7b.zip); src/common.mk uses
     #                        toolchains/cwsdpmi/bin/bin/CWSDSTUB.EXE
    +#   toolchains/upx       UPX 4.2.4 (amd64 Linux release), which packs
    +#                        SETUP.EXE (src/installer/Makefile)
     #   toolchains/86box     86Box v6.0 (build 9001) x86_64 Linux AppImage,
     #                        extracted once into app/ (no FUSE needed), and
     #                        the matching 86Box ROM set (tag v6.0) in roms/
    @@ -53,6 +55,10 @@
     #   toolchains/soundfont installMusicRefs.sh, which pins them): the
     #                        emu8950 OPL2 emulator (host suite) and a General
     #                        MIDI sound font for 86Box's MIDI (DOS tests)
    +#   toolchains/freetype  the fonts' inputs (src/fonts/installFonts.sh,
    +#   toolchains/fonts     which pins them): FreeType for the font converter,
    +#                        built from source, and every font DVX converts
    +#                        and ships, with its licence (src/fonts/sources.txt)
     #
     # Every download is checked against a pinned SHA-256 before it is unpacked,
     # so a changed or corrupted archive is refused instead of installed.
    @@ -70,6 +76,10 @@ DJGPP_SHA256="8464f17017d6ab1b2bb2df4ed82357b5bf692e6e2b7fee37e315638f3d505f00"
     DJGPP_VERSION="12.2.0"
     CWSDPMI_URL="https://sandmann.dotster.com/cwsdpmi/csdpmi7b.zip"
     CWSDPMI_SHA256="deacda0488e1cdd7c4a9f32fab45662b34c0ed6b2d7d4d13bc07041b62004a8c"
    +UPX_URL="https://github.com/upx/upx/releases/download/v4.2.4/upx-4.2.4-amd64_linux.tar.xz"
    +UPX_FILE="upx-4.2.4-amd64_linux.tar.xz"
    +UPX_SHA256="75cab4e57ab72fb4585ee45ff36388d280c7afd72aa03e8d4b9c3cbddb474193"
    +UPX_VERSION="4.2.4"
     BOX86_URL="https://github.com/86Box/86Box/releases/download/v6.0/86Box-Linux-x86_64-b9001.AppImage"
     BOX86_FILE="86Box-Linux-x86_64-b9001.AppImage"
     BOX86_SHA256="535d3c329f8c32b601103970c86d80aa80298f345452c3524ea2f433624727a4"
    @@ -103,6 +113,8 @@ DJGPP_DIR="${TOOLCHAINS}/djgpp"
     CWSDPMI_DIR="${TOOLCHAINS}/cwsdpmi"
     DJGPP_GCC="${DJGPP_DIR}/bin/i586-pc-msdosdjgpp-gcc"
     CWSDSTUB="${CWSDPMI_DIR}/bin/bin/CWSDSTUB.EXE"
    +UPX_DIR="${TOOLCHAINS}/upx"
    +UPX_BIN="${UPX_DIR}/upx"
     
     # Emulator, FreeDOS and cppcheck locations and the isolated run environment
     DVX_ROOT="${ROOT}"
    @@ -200,6 +212,20 @@ else
         echo "installToolchain: CWSDPMI installed in ${CWSDPMI_DIR}"
     fi
     
    +# UPX (the release binary, with its licence files)
    +if [ "${FORCE}" -eq 0 ] && pinned "${UPX_DIR}" "${UPX_SHA256}" && [ -x "${UPX_BIN}" ]; then
    +    echo "installToolchain: UPX ${UPX_VERSION} already installed"
    +else
    +    fetch "${UPX_URL}" "${UPX_FILE}" "${UPX_SHA256}"
    +    rm -rf "${UPX_DIR}.new"
    +    mkdir -p "${UPX_DIR}.new"
    +    tar -xJf "${CACHE}/${UPX_FILE}" -C "${UPX_DIR}.new" --strip-components=1
    +    rm -rf "${UPX_DIR}"
    +    mv "${UPX_DIR}.new" "${UPX_DIR}"
    +    echo "${UPX_SHA256}" > "${UPX_DIR}/${PIN_FILE}"
    +    echo "installToolchain: UPX installed in ${UPX_DIR}"
    +fi
    +
     # 86Box AppImage, extracted once so it runs without FUSE
     if [ "${FORCE}" -eq 0 ] && pinned "${BOX86_APP}" "${BOX86_SHA256}" && [ -x "${BOX86_BIN}" ]; then
         echo "installToolchain: 86Box already installed"
    @@ -313,19 +339,31 @@ else
         "${ROOT}/src/test/music/installMusicRefs.sh"
     fi
     
    +# FreeType and the fonts, pinned in their own script
    +if [ "${FORCE}" -eq 1 ]; then
    +    "${ROOT}/src/fonts/installFonts.sh" --force
    +else
    +    "${ROOT}/src/fonts/installFonts.sh"
    +fi
    +
     # Verify what the build needs.
     if [ "$("${DJGPP_GCC}" -dumpfullversion)" != "${DJGPP_VERSION}" ]; then
         echo "installToolchain: ${DJGPP_GCC} is not GCC ${DJGPP_VERSION}" >&2
         exit 1
     fi
     
    -for f in "${DJGPP_DIR}/i586-pc-msdosdjgpp/bin/dxe3gen" "${DJGPP_DIR}/i586-pc-msdosdjgpp/bin/exe2coff" "${CWSDSTUB}" "${BOX86_BIN}" "${BOX86_ROMS}/machines" "${FREEDOS_DIR}/MBR.BIN" "${FREEDOS_DIR}/BOOTSECT.BIN" "${FREEDOS_DIR}/KERNEL.SYS" "${FREEDOS_DIR}/COMMAND.COM" "${FREEDOS_DIR}/HIMEMX.EXE" "${FREEDOS_DIR}/UDVD2.SYS" "${FREEDOS_DIR}/SHSUCDX.COM" "${FREEDOS_DIR}/CTMOUSE.EXE" "${DOSBOX_BIN}" "${CPPCHECK_BIN}" "${TOOLCHAINS}/lrzsz/bin/lsz" "${TOOLCHAINS}/pktdrv/NE2000.COM" "${TOOLCHAINS}/emu8950/emu8950.c" "${TOOLCHAINS}/soundfont/TimGM6mb.sf2"; do
    +for f in "${DJGPP_DIR}/i586-pc-msdosdjgpp/bin/dxe3gen" "${DJGPP_DIR}/i586-pc-msdosdjgpp/bin/exe2coff" "${CWSDSTUB}" "${UPX_BIN}" "${BOX86_BIN}" "${BOX86_ROMS}/machines" "${FREEDOS_DIR}/MBR.BIN" "${FREEDOS_DIR}/BOOTSECT.BIN" "${FREEDOS_DIR}/KERNEL.SYS" "${FREEDOS_DIR}/COMMAND.COM" "${FREEDOS_DIR}/HIMEMX.EXE" "${FREEDOS_DIR}/UDVD2.SYS" "${FREEDOS_DIR}/SHSUCDX.COM" "${FREEDOS_DIR}/CTMOUSE.EXE" "${DOSBOX_BIN}" "${CPPCHECK_BIN}" "${TOOLCHAINS}/lrzsz/bin/lsz" "${TOOLCHAINS}/pktdrv/NE2000.COM" "${TOOLCHAINS}/emu8950/emu8950.c" "${TOOLCHAINS}/soundfont/TimGM6mb.sf2" "${TOOLCHAINS}/freetype/lib/libfreetype.a" "${TOOLCHAINS}/fonts/.installed"; do
         if [ ! -e "${f}" ]; then
             echo "installToolchain: expected file missing after install: ${f}" >&2
             exit 1
         fi
     done
     
    +if [ "$("${UPX_BIN}" --version | head -1)" != "upx ${UPX_VERSION}" ]; then
    +    echo "installToolchain: ${UPX_BIN} is not UPX ${UPX_VERSION}" >&2
    +    exit 1
    +fi
    +
     case "$("${CPPCHECK_BIN}" --version)" in
         "Cppcheck ${CPPCHECK_VERSION}"|"Cppcheck ${CPPCHECK_VERSION%.0}") ;;
         *)
    @@ -362,4 +400,4 @@ fi
     rm -rf "${checkDir}"
     echo "installToolchain: 86Box ran isolated (nothing written outside ${DVX_ROOT})"
     
    -echo "installToolchain: toolchain ready (GCC ${DJGPP_VERSION}, CWSDPMI r7, 86Box 6.0, FreeDOS 1.3, DOSBox Staging 0.83.0, cppcheck ${CPPCHECK_VERSION})"
    +echo "installToolchain: toolchain ready (GCC ${DJGPP_VERSION}, CWSDPMI r7, UPX ${UPX_VERSION}, 86Box 6.0, FreeDOS 1.3, DOSBox Staging 0.83.0, cppcheck ${CPPCHECK_VERSION})"
    diff --git a/screenshots/86box/apps-basdemo.png b/screenshots/86box/apps-basdemo.png
    index 1f073d2c..e5c56d36 100644
    --- a/screenshots/86box/apps-basdemo.png
    +++ b/screenshots/86box/apps-basdemo.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:887c02004051c2da25539cc3624e62ceda604bd27a8b5ed9a7fbaa3385d99c4a
    -size 16677
    +oid sha256:3e2bda6c643d300e3c46857547de4ae4d3e82ddb29056235832f6afcf4aac940
    +size 16813
    diff --git a/screenshots/86box/apps-dvxhelp.png b/screenshots/86box/apps-dvxhelp.png
    index b74dcf9f..1bd4ab98 100644
    --- a/screenshots/86box/apps-dvxhelp.png
    +++ b/screenshots/86box/apps-dvxhelp.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:1f7f9582710412a5edfc7b1937c40900ad051b7783d0351f14373c0f3e23ba09
    -size 21778
    +oid sha256:5e2c8e671952f6ddfb618246e4b20fb37acc9dc23aa3f9a1616a188a78f3a974
    +size 21764
    diff --git a/screenshots/86box/apps-filemgr.png b/screenshots/86box/apps-filemgr.png
    index 7aaac1d4..44e40d7c 100644
    --- a/screenshots/86box/apps-filemgr.png
    +++ b/screenshots/86box/apps-filemgr.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:5404afcf80fb3118a908e7727fbf1c23d349476fd9b25f11c2ae061e4b8d3b71
    -size 19573
    +oid sha256:817f6853d14090e39e40d08a6e93d6b168dda0084d89cc64672c2118d3712d94
    +size 20236
    diff --git a/screenshots/86box/apps-welcome.png b/screenshots/86box/apps-welcome.png
    index abd2ab8f..8fca1d66 100644
    --- a/screenshots/86box/apps-welcome.png
    +++ b/screenshots/86box/apps-welcome.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6ad65605b7b0d10a2fa4ce1e59a6255b1667dfef8a53fd1be0efb7e7a1f3957f
    -size 19200
    +oid sha256:84fe52aab2f648ad623509b2be7e39ff5d5809680566e1aaf6f14753b1af3057
    +size 21809
    diff --git a/screenshots/86box/cpanel-cp-video.png b/screenshots/86box/cpanel-cp-video.png
    index 8d658949..94174e34 100644
    --- a/screenshots/86box/cpanel-cp-video.png
    +++ b/screenshots/86box/cpanel-cp-video.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:69e8ed2e0257779ded5b0515b45691ef4ce92c5417e8a65b5b7db70ae6550ef8
    -size 19567
    +oid sha256:9c7ca998583545535beb72a7cd1347ef66917f213df5a72783079a805cf0a142
    +size 19483
    diff --git a/screenshots/86box/f1-h-basdem.png b/screenshots/86box/f1-h-basdem.png
    index f4a7268a..f178ed94 100644
    --- a/screenshots/86box/f1-h-basdem.png
    +++ b/screenshots/86box/f1-h-basdem.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:35fc158b43b5d885c56e667e5bb68ebc1c50d0f08889e0169da011d0cdb3c89e
    -size 20672
    +oid sha256:f5b3bc24e986cc704e74e6b7dd72b8f42ddc87c9a88e7c535d5c5d94d6c2c51c
    +size 20728
    diff --git a/screenshots/86box/f1-h-clock.png b/screenshots/86box/f1-h-clock.png
    index 9804434e..e293aa1d 100644
    --- a/screenshots/86box/f1-h-clock.png
    +++ b/screenshots/86box/f1-h-clock.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:5dc1025c9c352e5ef356183809bdf46adbf5eb8e5d53962cb2ced51af725396b
    -size 18925
    +oid sha256:0ce6bf30393be569792f8c906b1fe1506ee09aa304d58a1c5ac2c233bfd28d28
    +size 18940
    diff --git a/screenshots/86box/f1-h-cpanel.png b/screenshots/86box/f1-h-cpanel.png
    index 73217281..a2a88cdf 100644
    --- a/screenshots/86box/f1-h-cpanel.png
    +++ b/screenshots/86box/f1-h-cpanel.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6e7bbc951b075d68fc197dad925013d6cb1d87c841ced7d5df7c9814e2eb8bf6
    -size 22615
    +oid sha256:c608a08ba5206a0cb5bfbb9c4845087b99fae8d9dff28323fc161fc2a48a5aab
    +size 22602
    diff --git a/screenshots/86box/f1-h-filemg.png b/screenshots/86box/f1-h-filemg.png
    index ead0ae03..97812e25 100644
    --- a/screenshots/86box/f1-h-filemg.png
    +++ b/screenshots/86box/f1-h-filemg.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:ec722748514a87b45bc664300913eb30805bd55e87a2902b6bb6746110611a0e
    -size 18162
    +oid sha256:597f8eb1f7b253495ba7582b8c4eeedce930b3fd269b524354105df1704546b1
    +size 18209
    diff --git a/screenshots/86box/f1-h-helped.png b/screenshots/86box/f1-h-helped.png
    index bdd51317..edb34af5 100644
    --- a/screenshots/86box/f1-h-helped.png
    +++ b/screenshots/86box/f1-h-helped.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:bc316f8c448a6da952568e02e50c062bf7de0541912a50943f02de76dabaf83f
    -size 22492
    +oid sha256:a049c2d9a6eabcd56d1c8144316cb009cc4a579596d64913d2ca474ca70e503a
    +size 22468
    diff --git a/screenshots/86box/f1-h-imgvie.png b/screenshots/86box/f1-h-imgvie.png
    index aed8c940..2997b10f 100644
    --- a/screenshots/86box/f1-h-imgvie.png
    +++ b/screenshots/86box/f1-h-imgvie.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:9d70681b9e13ae003bf590de58b329d705e3e5c138f396dd14619d123f261bbc
    -size 19246
    +oid sha256:551335eede657c3a0ba3d34ea23d8134214f3b7914510cdd02d417f14cd949dc
    +size 19273
    diff --git a/screenshots/86box/f1-h-notepa.png b/screenshots/86box/f1-h-notepa.png
    index ec446878..edb6ee26 100644
    --- a/screenshots/86box/f1-h-notepa.png
    +++ b/screenshots/86box/f1-h-notepa.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:13a7ecd0bcb094b1ddc0cc08aac8d196eee6e02164a39d4089ab7f0dd8587be6
    -size 18951
    +oid sha256:8a377752193cd085742ecf9a616c990e897189c20443ca754434a3fb036c47d3
    +size 18895
    diff --git a/screenshots/86box/f1-h-resedi.png b/screenshots/86box/f1-h-resedi.png
    index 3b9a69d4..3e519eee 100644
    --- a/screenshots/86box/f1-h-resedi.png
    +++ b/screenshots/86box/f1-h-resedi.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:dda3e16b4aa8628682e61bc1263ecb37bc0823e54a96dceee091cc1e90c30b0c
    -size 20872
    +oid sha256:ae20d685ccbd0230a20c48efa684c0fde6ae1eed48aeaf1a6fedc81b4c2ede50
    +size 20875
    diff --git a/screenshots/86box/f1-h-welcom.png b/screenshots/86box/f1-h-welcom.png
    index b772f1b0..8518787f 100644
    --- a/screenshots/86box/f1-h-welcom.png
    +++ b/screenshots/86box/f1-h-welcom.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:242657f05cc4b6fc531ecef24f6e2639e169675371f282af275973cc9ebdb38e
    -size 18913
    +oid sha256:b1b3758c817e427e44cbc91837a48f04d88548435da0854d27dded272235584c
    +size 18884
    diff --git a/screenshots/86box/filedlg-fd-back.png b/screenshots/86box/filedlg-fd-back.png
    index 482d1115..97c62ebf 100644
    --- a/screenshots/86box/filedlg-fd-back.png
    +++ b/screenshots/86box/filedlg-fd-back.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:48c7b0b31ec7922f1364cf3e0175896190a6394ad27146ccfa62b0c500465d35
    -size 15077
    +oid sha256:8627ce74f1235a78c96cc29b927f2ff14c5e3618c6f939d30ce1f14fcad6340f
    +size 15223
    diff --git a/screenshots/86box/filedlg-fd-list.png b/screenshots/86box/filedlg-fd-list.png
    index 7b764871..975c0f49 100644
    --- a/screenshots/86box/filedlg-fd-list.png
    +++ b/screenshots/86box/filedlg-fd-list.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:d9f468b5c0772dfc324784e10376c88a526a92999de6c6e47b5db09918bdc046
    -size 15173
    +oid sha256:1fe635da33c5961124b95a4eb0664eef1cf2e08e4665c834e3b2692e39f0e94b
    +size 15347
    diff --git a/screenshots/86box/filedlg-fd-open.png b/screenshots/86box/filedlg-fd-open.png
    index ff2fc78d..4efebdea 100644
    --- a/screenshots/86box/filedlg-fd-open.png
    +++ b/screenshots/86box/filedlg-fd-open.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:b738091c36cea453d9a4aced6ce293bc1e2d073402bc302bebdba8910ecac14a
    -size 15028
    +oid sha256:75f3a5f244774265a0ae03e264dfd358e8be4a05d5238ee7ba818436eac25f49
    +size 15182
    diff --git a/screenshots/86box/firstrun-fr-open.png b/screenshots/86box/firstrun-fr-open.png
    index abd2ab8f..8fca1d66 100644
    --- a/screenshots/86box/firstrun-fr-open.png
    +++ b/screenshots/86box/firstrun-fr-open.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6ad65605b7b0d10a2fa4ce1e59a6255b1667dfef8a53fd1be0efb7e7a1f3957f
    -size 19200
    +oid sha256:84fe52aab2f648ad623509b2be7e39ff5d5809680566e1aaf6f14753b1af3057
    +size 21809
    diff --git a/screenshots/86box/fonts-basic.png b/screenshots/86box/fonts-basic.png
    new file mode 100644
    index 00000000..27e9ec9e
    --- /dev/null
    +++ b/screenshots/86box/fonts-basic.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:dbc32f5465cca9d6b7a9235a04d5b2de8ce94abd3f1d91975658fe8e1ac8e545
    +size 19792
    diff --git a/screenshots/86box/fonts-demo.png b/screenshots/86box/fonts-demo.png
    new file mode 100644
    index 00000000..1159ebe7
    --- /dev/null
    +++ b/screenshots/86box/fonts-demo.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:b6d6e68f1c2fae10f0a860f12c81e2c6ac3ff51db89faff3c3a0fa3f04df8ddc
    +size 21542
    diff --git a/screenshots/86box/fonts-edit.png b/screenshots/86box/fonts-edit.png
    new file mode 100644
    index 00000000..06a5daf0
    --- /dev/null
    +++ b/screenshots/86box/fonts-edit.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:891a1d1cbdc858c5786106c12e08f54cc2ff7d070420f6edaafdd0c6bab49be9
    +size 15192
    diff --git a/screenshots/86box/fonts8-basic.png b/screenshots/86box/fonts8-basic.png
    new file mode 100644
    index 00000000..025515da
    --- /dev/null
    +++ b/screenshots/86box/fonts8-basic.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:acc2cb3d654f8570f2cc8e353a442ee203a1f4a0125ac250c1a3098c2ef072de
    +size 19008
    diff --git a/screenshots/86box/fonts8-demo.png b/screenshots/86box/fonts8-demo.png
    new file mode 100644
    index 00000000..ec496d85
    --- /dev/null
    +++ b/screenshots/86box/fonts8-demo.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:03de669aa08a47777ca708381042ca7d8fdb21ada9e30054ee500d3d34ccc9e7
    +size 17884
    diff --git a/screenshots/86box/fonts8-edit.png b/screenshots/86box/fonts8-edit.png
    new file mode 100644
    index 00000000..3296991c
    --- /dev/null
    +++ b/screenshots/86box/fonts8-edit.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:2a36c8507a21d43c59f51cc60c42e0b2f11ced35c329c014889e72e5d6852ba5
    +size 14743
    diff --git a/screenshots/86box/install-in-welc.png b/screenshots/86box/install-in-welc.png
    index abd2ab8f..8fca1d66 100644
    --- a/screenshots/86box/install-in-welc.png
    +++ b/screenshots/86box/install-in-welc.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6ad65605b7b0d10a2fa4ce1e59a6255b1667dfef8a53fd1be0efb7e7a1f3957f
    -size 19200
    +oid sha256:84fe52aab2f648ad623509b2be7e39ff5d5809680566e1aaf6f14753b1af3057
    +size 21809
    diff --git a/screenshots/86box/install-su-copy.png b/screenshots/86box/install-su-copy.png
    index e41442e3..1fee6c29 100644
    --- a/screenshots/86box/install-su-copy.png
    +++ b/screenshots/86box/install-su-copy.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:c5a3aedf87a9d95bfe86d681049462cd11c66bb82e634088bdcb0d907e4bea38
    -size 17127
    +oid sha256:fdbf49cb682467b8fd4f1f8e34fcb449fd5fff43cb0d334e1a33640ed77c0dcc
    +size 24503
    diff --git a/screenshots/86box/install-su-done.png b/screenshots/86box/install-su-done.png
    index 436cf1ca..65c53b03 100644
    --- a/screenshots/86box/install-su-done.png
    +++ b/screenshots/86box/install-su-done.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:82de2cdff532a7be265cf8ca9fb39d212677b393b64fbb191cdcd5674584c077
    -size 16310
    +oid sha256:9dc12906f9d8052de2f6890597bf868e4c69ceaf5e0f2b46dc1d33d5794f2b25
    +size 22957
    diff --git a/screenshots/86box/install-su-fold.png b/screenshots/86box/install-su-fold.png
    index 80af0974..e46b670f 100644
    --- a/screenshots/86box/install-su-fold.png
    +++ b/screenshots/86box/install-su-fold.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:02cefd9f80b7b54d90d5614c08c570b25a26979c2dea6e1c932058e7f5122434
    -size 16454
    +oid sha256:d0e4194fee2f4b9853464a41f83fe88aa5c65d9578c0d7ad86e315ebe90f2cc4
    +size 21772
    diff --git a/screenshots/86box/install-su-lic.png b/screenshots/86box/install-su-lic.png
    index 52dd988a..a5f80c35 100644
    --- a/screenshots/86box/install-su-lic.png
    +++ b/screenshots/86box/install-su-lic.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6082149d59c1fd4a4fa2b5333522434d2703ee0e29f653a66208d747301b5f74
    -size 17327
    +oid sha256:28003d2ec8561fcece2bec259abefa831b1b2ba1e38c446963a1872512e0315d
    +size 20521
    diff --git a/screenshots/86box/install-su-unin.png b/screenshots/86box/install-su-unin.png
    index af3047f5..9bebaef5 100644
    --- a/screenshots/86box/install-su-unin.png
    +++ b/screenshots/86box/install-su-unin.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:351484aaaa1aa48ab8f208d235677a2ccefeaaffb9ee4a12646e49e2157ccb3d
    -size 14947
    +oid sha256:4df2274a81a2dfa8864312f402d6899d72ed1669f67a884baa983a455f6dbcb3
    +size 18185
    diff --git a/screenshots/86box/instbase-demo.png b/screenshots/86box/instbase-demo.png
    new file mode 100644
    index 00000000..ce74ebd8
    --- /dev/null
    +++ b/screenshots/86box/instbase-demo.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:5e004e20c5fd07990538ff2c393efb75fe6f347e2d4f3503d5401988afb28885
    +size 21381
    diff --git a/screenshots/86box/instbase-fonts.png b/screenshots/86box/instbase-fonts.png
    new file mode 100644
    index 00000000..88b75706
    --- /dev/null
    +++ b/screenshots/86box/instbase-fonts.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:257380789d93e3dade5dd095cbcf148acc76b0dd809911bee6d3f0849fa0dc5c
    +size 19735
    diff --git a/screenshots/86box/instbase-su-copy.png b/screenshots/86box/instbase-su-copy.png
    new file mode 100644
    index 00000000..1fee6c29
    --- /dev/null
    +++ b/screenshots/86box/instbase-su-copy.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:fdbf49cb682467b8fd4f1f8e34fcb449fd5fff43cb0d334e1a33640ed77c0dcc
    +size 24503
    diff --git a/screenshots/86box/instbase-su-done.png b/screenshots/86box/instbase-su-done.png
    new file mode 100644
    index 00000000..65c53b03
    --- /dev/null
    +++ b/screenshots/86box/instbase-su-done.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:9dc12906f9d8052de2f6890597bf868e4c69ceaf5e0f2b46dc1d33d5794f2b25
    +size 22957
    diff --git a/screenshots/86box/instbase-su-fold.png b/screenshots/86box/instbase-su-fold.png
    new file mode 100644
    index 00000000..75848fe9
    --- /dev/null
    +++ b/screenshots/86box/instbase-su-fold.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:90ff9a56778f1e60fe530830089c48ec0c46f5dcd6ecc1755c362cc7ae839689
    +size 21774
    diff --git a/screenshots/86box/instbase-su-lic.png b/screenshots/86box/instbase-su-lic.png
    new file mode 100644
    index 00000000..a5f80c35
    --- /dev/null
    +++ b/screenshots/86box/instbase-su-lic.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:28003d2ec8561fcece2bec259abefa831b1b2ba1e38c446963a1872512e0315d
    +size 20521
    diff --git a/screenshots/86box/instbase-su-unin.png b/screenshots/86box/instbase-su-unin.png
    new file mode 100644
    index 00000000..9bebaef5
    --- /dev/null
    +++ b/screenshots/86box/instbase-su-unin.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:4df2274a81a2dfa8864312f402d6899d72ed1669f67a884baa983a455f6dbcb3
    +size 18185
    diff --git a/screenshots/86box/menus-m-design.png b/screenshots/86box/menus-m-design.png
    index c01e4407..917b6fca 100644
    --- a/screenshots/86box/menus-m-design.png
    +++ b/screenshots/86box/menus-m-design.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:8cdbffd36d02a2fd16b3e86d1bcb90c487f0440e30898ebddf21bb61b26861a1
    -size 23000
    +oid sha256:697d37a38da9c8c668a14d77c9b3a80345dc54df17a5377d6af8dca2b1146442
    +size 23484
    diff --git a/screenshots/86box/sdk-sdktbx.png b/screenshots/86box/sdk-sdktbx.png
    index 06535181..80120caf 100644
    --- a/screenshots/86box/sdk-sdktbx.png
    +++ b/screenshots/86box/sdk-sdktbx.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:b636eef5ae0dc8fe5c132c2b7d37c94ccdfa536f72d70b83b6cc2c0fc647d59c
    -size 22905
    +oid sha256:68680747bb40b2de6f9aaf00d8589f107318e126eba5f4ded50b3003ec3a8e64
    +size 23397
    diff --git a/screenshots/86box/telnet-telnet.png b/screenshots/86box/telnet-telnet.png
    index 3a30d0a4..52c0fc04 100644
    --- a/screenshots/86box/telnet-telnet.png
    +++ b/screenshots/86box/telnet-telnet.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:e9f77a5606a378645c0e86d2f2849cb7eccccc06efc8bdcf40fa6d2f5ab8034e
    -size 13578
    +oid sha256:b8679baf25a7213f266c44131d3549769a81e16086d89d5d28b067449c0a9ddb
    +size 13739
    diff --git a/screenshots/86box/ucide-ucide-ds.png b/screenshots/86box/ucide-ucide-ds.png
    index 06915901..70d3877e 100644
    --- a/screenshots/86box/ucide-ucide-ds.png
    +++ b/screenshots/86box/ucide-ucide-ds.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:524ccf39bc12ab0172c480795b7053cd8dcf3aae9fea34320c9a679b815315f1
    -size 31120
    +oid sha256:0979cbe27301ac2299b437d14fbf6850bad838a150c206f58fc81f673c01d5e4
    +size 31507
    diff --git a/screenshots/86box/ucide-ucide-tb.png b/screenshots/86box/ucide-ucide-tb.png
    index 94c5c616..e147e726 100644
    --- a/screenshots/86box/ucide-ucide-tb.png
    +++ b/screenshots/86box/ucide-ucide-tb.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:e0847ddd48455ad7a16b279e86aa7faaacaaa94acc563ea635aacaef53ee330a
    -size 31611
    +oid sha256:58103050f0a0174d722831a2a7771841662980826275be5a220c21816b115b94
    +size 31620
    diff --git a/screenshots/86box/upgrade-su-copy.png b/screenshots/86box/upgrade-su-copy.png
    index e41442e3..1fee6c29 100644
    --- a/screenshots/86box/upgrade-su-copy.png
    +++ b/screenshots/86box/upgrade-su-copy.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:c5a3aedf87a9d95bfe86d681049462cd11c66bb82e634088bdcb0d907e4bea38
    -size 17127
    +oid sha256:fdbf49cb682467b8fd4f1f8e34fcb449fd5fff43cb0d334e1a33640ed77c0dcc
    +size 24503
    diff --git a/screenshots/86box/upgrade-su-done.png b/screenshots/86box/upgrade-su-done.png
    index 436cf1ca..65c53b03 100644
    --- a/screenshots/86box/upgrade-su-done.png
    +++ b/screenshots/86box/upgrade-su-done.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:82de2cdff532a7be265cf8ca9fb39d212677b393b64fbb191cdcd5674584c077
    -size 16310
    +oid sha256:9dc12906f9d8052de2f6890597bf868e4c69ceaf5e0f2b46dc1d33d5794f2b25
    +size 22957
    diff --git a/screenshots/86box/upgrade-su-fold.png b/screenshots/86box/upgrade-su-fold.png
    index 80af0974..e46b670f 100644
    --- a/screenshots/86box/upgrade-su-fold.png
    +++ b/screenshots/86box/upgrade-su-fold.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:02cefd9f80b7b54d90d5614c08c570b25a26979c2dea6e1c932058e7f5122434
    -size 16454
    +oid sha256:d0e4194fee2f4b9853464a41f83fe88aa5c65d9578c0d7ad86e315ebe90f2cc4
    +size 21772
    diff --git a/screenshots/86box/upgrade-su-lic.png b/screenshots/86box/upgrade-su-lic.png
    index 52dd988a..a5f80c35 100644
    --- a/screenshots/86box/upgrade-su-lic.png
    +++ b/screenshots/86box/upgrade-su-lic.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6082149d59c1fd4a4fa2b5333522434d2703ee0e29f653a66208d747301b5f74
    -size 17327
    +oid sha256:28003d2ec8561fcece2bec259abefa831b1b2ba1e38c446963a1872512e0315d
    +size 20521
    diff --git a/screenshots/86box/upgrade-su-unin.png b/screenshots/86box/upgrade-su-unin.png
    index af3047f5..9bebaef5 100644
    --- a/screenshots/86box/upgrade-su-unin.png
    +++ b/screenshots/86box/upgrade-su-unin.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:351484aaaa1aa48ab8f208d235677a2ccefeaaffb9ee4a12646e49e2157ccb3d
    -size 14947
    +oid sha256:4df2274a81a2dfa8864312f402d6899d72ed1669f67a884baa983a455f6dbcb3
    +size 18185
    diff --git a/screenshots/86box/upgrade-su-upgr.png b/screenshots/86box/upgrade-su-upgr.png
    index 55bc5afa..83ee1500 100644
    --- a/screenshots/86box/upgrade-su-upgr.png
    +++ b/screenshots/86box/upgrade-su-upgr.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:1030284051814c5abadf8babc30a9d92671cf0c68bdf17d4287c8cf5a0c92363
    -size 16882
    +oid sha256:e0a8f8c12e567eabcf2dc238b375401a4a7db77e1a868c3bba0397e0e752c8e8
    +size 22144
    diff --git a/screenshots/86box/video-v-800.png b/screenshots/86box/video-v-800.png
    index 47a6a6e8..3fda5f53 100644
    --- a/screenshots/86box/video-v-800.png
    +++ b/screenshots/86box/video-v-800.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:0a97c8b6db807df057d4b1acd9d48e9e04bdd5da64d537d5b76dc51a1942fe52
    -size 26516
    +oid sha256:4ed7a7573e5fe8b5ec5d49ad759ea7253f6be9a79b7473a32ca8694a674d1666
    +size 26164
    diff --git a/screenshots/86box/video-v-pick.png b/screenshots/86box/video-v-pick.png
    index 7d78961a..b9bb6f8a 100644
    --- a/screenshots/86box/video-v-pick.png
    +++ b/screenshots/86box/video-v-pick.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:802f7e81d1aa1b6bfc9661a9fef4824c44513f3e271d0be2825e02f16c25900c
    -size 20174
    +oid sha256:c37a4db6ca3c28af5e71682f886eab44d3fe5adbc92e008bc1520fb54d994eb3
    +size 20076
    diff --git a/screenshots/86box/welcome-wl-help.png b/screenshots/86box/welcome-wl-help.png
    index b772f1b0..8518787f 100644
    --- a/screenshots/86box/welcome-wl-help.png
    +++ b/screenshots/86box/welcome-wl-help.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:242657f05cc4b6fc531ecef24f6e2639e169675371f282af275973cc9ebdb38e
    -size 18913
    +oid sha256:b1b3758c817e427e44cbc91837a48f04d88548435da0854d27dded272235584c
    +size 18884
    diff --git a/screenshots/86box/welcome-wl-open.png b/screenshots/86box/welcome-wl-open.png
    index abd2ab8f..8fca1d66 100644
    --- a/screenshots/86box/welcome-wl-open.png
    +++ b/screenshots/86box/welcome-wl-open.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6ad65605b7b0d10a2fa4ce1e59a6255b1667dfef8a53fd1be0efb7e7a1f3957f
    -size 19200
    +oid sha256:84fe52aab2f648ad623509b2be7e39ff5d5809680566e1aaf6f14753b1af3057
    +size 21809
    diff --git a/screenshots/86box/welcome-wl-tour.png b/screenshots/86box/welcome-wl-tour.png
    index a772093c..ff0fbc0a 100644
    --- a/screenshots/86box/welcome-wl-tour.png
    +++ b/screenshots/86box/welcome-wl-tour.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:7a0e03692d92fbc8b95a858c72e7fb83c169bf8f19b4c5462be5f4ed6e5dff08
    -size 18342
    +oid sha256:c3eda8d3a118877e4417c8c9ed52fd4530e6cab356482ff0d8ce6cdc8cdeece1
    +size 24645
    diff --git a/screenshots/86box/welcome-wl-tour2.png b/screenshots/86box/welcome-wl-tour2.png
    index d9f17e84..e9c8d62f 100644
    --- a/screenshots/86box/welcome-wl-tour2.png
    +++ b/screenshots/86box/welcome-wl-tour2.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:c63e835f1d7da0b190771c96b32c7dd532e19e3dd741ac25218361ab0055720b
    -size 18588
    +oid sha256:858c116aa5e55367589908c9f0c3dea6cf3adf0174d7f3876eb306c45db7a261
    +size 24465
    diff --git a/screenshots/dosbox/apps-basdemo.png b/screenshots/dosbox/apps-basdemo.png
    index 1f073d2c..e5c56d36 100644
    --- a/screenshots/dosbox/apps-basdemo.png
    +++ b/screenshots/dosbox/apps-basdemo.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:887c02004051c2da25539cc3624e62ceda604bd27a8b5ed9a7fbaa3385d99c4a
    -size 16677
    +oid sha256:3e2bda6c643d300e3c46857547de4ae4d3e82ddb29056235832f6afcf4aac940
    +size 16813
    diff --git a/screenshots/dosbox/apps-dvxhelp.png b/screenshots/dosbox/apps-dvxhelp.png
    index b74dcf9f..1bd4ab98 100644
    --- a/screenshots/dosbox/apps-dvxhelp.png
    +++ b/screenshots/dosbox/apps-dvxhelp.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:1f7f9582710412a5edfc7b1937c40900ad051b7783d0351f14373c0f3e23ba09
    -size 21778
    +oid sha256:5e2c8e671952f6ddfb618246e4b20fb37acc9dc23aa3f9a1616a188a78f3a974
    +size 21764
    diff --git a/screenshots/dosbox/apps-filemgr.png b/screenshots/dosbox/apps-filemgr.png
    index 906a0565..a21afbc9 100644
    --- a/screenshots/dosbox/apps-filemgr.png
    +++ b/screenshots/dosbox/apps-filemgr.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:df3900eac84b6b6841c28be5b86c7e8bddda7a12941082c185cc4973064d0ee9
    -size 19772
    +oid sha256:0d2ae9ee2c05f4f96a7061dda5ff7b176c9359c3907a0163dcfa9b27bceaaeeb
    +size 20247
    diff --git a/screenshots/dosbox/apps-welcome.png b/screenshots/dosbox/apps-welcome.png
    index abd2ab8f..8fca1d66 100644
    --- a/screenshots/dosbox/apps-welcome.png
    +++ b/screenshots/dosbox/apps-welcome.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6ad65605b7b0d10a2fa4ce1e59a6255b1667dfef8a53fd1be0efb7e7a1f3957f
    -size 19200
    +oid sha256:84fe52aab2f648ad623509b2be7e39ff5d5809680566e1aaf6f14753b1af3057
    +size 21809
    diff --git a/screenshots/dosbox/cpanel-cp-video.png b/screenshots/dosbox/cpanel-cp-video.png
    index f3bb8a38..12d44ca2 100644
    --- a/screenshots/dosbox/cpanel-cp-video.png
    +++ b/screenshots/dosbox/cpanel-cp-video.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:ed7c9e4142fba46ed3ebfc49d5dd984823ccece4c7c8f6aab6bbc0298ab2d3ad
    -size 19744
    +oid sha256:f8c9d977e1af0dacdcf0ce9d40f74a88f1fe57c65a0468e7b2295f39fbaf9720
    +size 19767
    diff --git a/screenshots/dosbox/f1-h-basdem.png b/screenshots/dosbox/f1-h-basdem.png
    index f4a7268a..f178ed94 100644
    --- a/screenshots/dosbox/f1-h-basdem.png
    +++ b/screenshots/dosbox/f1-h-basdem.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:35fc158b43b5d885c56e667e5bb68ebc1c50d0f08889e0169da011d0cdb3c89e
    -size 20672
    +oid sha256:f5b3bc24e986cc704e74e6b7dd72b8f42ddc87c9a88e7c535d5c5d94d6c2c51c
    +size 20728
    diff --git a/screenshots/dosbox/f1-h-clock.png b/screenshots/dosbox/f1-h-clock.png
    index 9804434e..e293aa1d 100644
    --- a/screenshots/dosbox/f1-h-clock.png
    +++ b/screenshots/dosbox/f1-h-clock.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:5dc1025c9c352e5ef356183809bdf46adbf5eb8e5d53962cb2ced51af725396b
    -size 18925
    +oid sha256:0ce6bf30393be569792f8c906b1fe1506ee09aa304d58a1c5ac2c233bfd28d28
    +size 18940
    diff --git a/screenshots/dosbox/f1-h-cpanel.png b/screenshots/dosbox/f1-h-cpanel.png
    index 73217281..a2a88cdf 100644
    --- a/screenshots/dosbox/f1-h-cpanel.png
    +++ b/screenshots/dosbox/f1-h-cpanel.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6e7bbc951b075d68fc197dad925013d6cb1d87c841ced7d5df7c9814e2eb8bf6
    -size 22615
    +oid sha256:c608a08ba5206a0cb5bfbb9c4845087b99fae8d9dff28323fc161fc2a48a5aab
    +size 22602
    diff --git a/screenshots/dosbox/f1-h-filemg.png b/screenshots/dosbox/f1-h-filemg.png
    index a6b4f0d9..12beaa2f 100644
    --- a/screenshots/dosbox/f1-h-filemg.png
    +++ b/screenshots/dosbox/f1-h-filemg.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:108fad7cfb25f3f83003aa735a16320ebe96fc2a14ac37832b7e372b5cf30b3d
    -size 18200
    +oid sha256:0c0af11b6ce5d3ebae02f44d51d8d2ffd19d8922d63b1023bc10811db1f16ef1
    +size 18135
    diff --git a/screenshots/dosbox/f1-h-helped.png b/screenshots/dosbox/f1-h-helped.png
    index bdd51317..edb34af5 100644
    --- a/screenshots/dosbox/f1-h-helped.png
    +++ b/screenshots/dosbox/f1-h-helped.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:bc316f8c448a6da952568e02e50c062bf7de0541912a50943f02de76dabaf83f
    -size 22492
    +oid sha256:a049c2d9a6eabcd56d1c8144316cb009cc4a579596d64913d2ca474ca70e503a
    +size 22468
    diff --git a/screenshots/dosbox/f1-h-imgvie.png b/screenshots/dosbox/f1-h-imgvie.png
    index aed8c940..2997b10f 100644
    --- a/screenshots/dosbox/f1-h-imgvie.png
    +++ b/screenshots/dosbox/f1-h-imgvie.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:9d70681b9e13ae003bf590de58b329d705e3e5c138f396dd14619d123f261bbc
    -size 19246
    +oid sha256:551335eede657c3a0ba3d34ea23d8134214f3b7914510cdd02d417f14cd949dc
    +size 19273
    diff --git a/screenshots/dosbox/f1-h-notepa.png b/screenshots/dosbox/f1-h-notepa.png
    index ec446878..edb6ee26 100644
    --- a/screenshots/dosbox/f1-h-notepa.png
    +++ b/screenshots/dosbox/f1-h-notepa.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:13a7ecd0bcb094b1ddc0cc08aac8d196eee6e02164a39d4089ab7f0dd8587be6
    -size 18951
    +oid sha256:8a377752193cd085742ecf9a616c990e897189c20443ca754434a3fb036c47d3
    +size 18895
    diff --git a/screenshots/dosbox/f1-h-resedi.png b/screenshots/dosbox/f1-h-resedi.png
    index 3b9a69d4..3e519eee 100644
    --- a/screenshots/dosbox/f1-h-resedi.png
    +++ b/screenshots/dosbox/f1-h-resedi.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:dda3e16b4aa8628682e61bc1263ecb37bc0823e54a96dceee091cc1e90c30b0c
    -size 20872
    +oid sha256:ae20d685ccbd0230a20c48efa684c0fde6ae1eed48aeaf1a6fedc81b4c2ede50
    +size 20875
    diff --git a/screenshots/dosbox/f1-h-welcom.png b/screenshots/dosbox/f1-h-welcom.png
    index b772f1b0..8518787f 100644
    --- a/screenshots/dosbox/f1-h-welcom.png
    +++ b/screenshots/dosbox/f1-h-welcom.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:242657f05cc4b6fc531ecef24f6e2639e169675371f282af275973cc9ebdb38e
    -size 18913
    +oid sha256:b1b3758c817e427e44cbc91837a48f04d88548435da0854d27dded272235584c
    +size 18884
    diff --git a/screenshots/dosbox/filedlg-fd-back.png b/screenshots/dosbox/filedlg-fd-back.png
    index 482d1115..97c62ebf 100644
    --- a/screenshots/dosbox/filedlg-fd-back.png
    +++ b/screenshots/dosbox/filedlg-fd-back.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:48c7b0b31ec7922f1364cf3e0175896190a6394ad27146ccfa62b0c500465d35
    -size 15077
    +oid sha256:8627ce74f1235a78c96cc29b927f2ff14c5e3618c6f939d30ce1f14fcad6340f
    +size 15223
    diff --git a/screenshots/dosbox/filedlg-fd-list.png b/screenshots/dosbox/filedlg-fd-list.png
    index 372a7b83..29674330 100644
    --- a/screenshots/dosbox/filedlg-fd-list.png
    +++ b/screenshots/dosbox/filedlg-fd-list.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:469c9fea60a56141a1515fd8e01085791abc29fae5a98ff379a3ab77da8dc34c
    -size 15262
    +oid sha256:b46dda7bded6fa634a6b2a42ee9ed9ac8252b625595b01da8c1a58e30a430313
    +size 15436
    diff --git a/screenshots/dosbox/filedlg-fd-open.png b/screenshots/dosbox/filedlg-fd-open.png
    index ff2fc78d..4efebdea 100644
    --- a/screenshots/dosbox/filedlg-fd-open.png
    +++ b/screenshots/dosbox/filedlg-fd-open.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:b738091c36cea453d9a4aced6ce293bc1e2d073402bc302bebdba8910ecac14a
    -size 15028
    +oid sha256:75f3a5f244774265a0ae03e264dfd358e8be4a05d5238ee7ba818436eac25f49
    +size 15182
    diff --git a/screenshots/dosbox/firstrun-fr-open.png b/screenshots/dosbox/firstrun-fr-open.png
    index abd2ab8f..8fca1d66 100644
    --- a/screenshots/dosbox/firstrun-fr-open.png
    +++ b/screenshots/dosbox/firstrun-fr-open.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6ad65605b7b0d10a2fa4ce1e59a6255b1667dfef8a53fd1be0efb7e7a1f3957f
    -size 19200
    +oid sha256:84fe52aab2f648ad623509b2be7e39ff5d5809680566e1aaf6f14753b1af3057
    +size 21809
    diff --git a/screenshots/dosbox/fonts-basic.png b/screenshots/dosbox/fonts-basic.png
    new file mode 100644
    index 00000000..27e9ec9e
    --- /dev/null
    +++ b/screenshots/dosbox/fonts-basic.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:dbc32f5465cca9d6b7a9235a04d5b2de8ce94abd3f1d91975658fe8e1ac8e545
    +size 19792
    diff --git a/screenshots/dosbox/fonts-demo.png b/screenshots/dosbox/fonts-demo.png
    new file mode 100644
    index 00000000..1159ebe7
    --- /dev/null
    +++ b/screenshots/dosbox/fonts-demo.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:b6d6e68f1c2fae10f0a860f12c81e2c6ac3ff51db89faff3c3a0fa3f04df8ddc
    +size 21542
    diff --git a/screenshots/dosbox/fonts-edit.png b/screenshots/dosbox/fonts-edit.png
    new file mode 100644
    index 00000000..06a5daf0
    --- /dev/null
    +++ b/screenshots/dosbox/fonts-edit.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:891a1d1cbdc858c5786106c12e08f54cc2ff7d070420f6edaafdd0c6bab49be9
    +size 15192
    diff --git a/screenshots/dosbox/fonts8-basic.png b/screenshots/dosbox/fonts8-basic.png
    new file mode 100644
    index 00000000..025515da
    --- /dev/null
    +++ b/screenshots/dosbox/fonts8-basic.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:acc2cb3d654f8570f2cc8e353a442ee203a1f4a0125ac250c1a3098c2ef072de
    +size 19008
    diff --git a/screenshots/dosbox/fonts8-demo.png b/screenshots/dosbox/fonts8-demo.png
    new file mode 100644
    index 00000000..ec496d85
    --- /dev/null
    +++ b/screenshots/dosbox/fonts8-demo.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:03de669aa08a47777ca708381042ca7d8fdb21ada9e30054ee500d3d34ccc9e7
    +size 17884
    diff --git a/screenshots/dosbox/fonts8-edit.png b/screenshots/dosbox/fonts8-edit.png
    new file mode 100644
    index 00000000..3296991c
    --- /dev/null
    +++ b/screenshots/dosbox/fonts8-edit.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:2a36c8507a21d43c59f51cc60c42e0b2f11ced35c329c014889e72e5d6852ba5
    +size 14743
    diff --git a/screenshots/dosbox/install-in-welc.png b/screenshots/dosbox/install-in-welc.png
    index abd2ab8f..8fca1d66 100644
    --- a/screenshots/dosbox/install-in-welc.png
    +++ b/screenshots/dosbox/install-in-welc.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6ad65605b7b0d10a2fa4ce1e59a6255b1667dfef8a53fd1be0efb7e7a1f3957f
    -size 19200
    +oid sha256:84fe52aab2f648ad623509b2be7e39ff5d5809680566e1aaf6f14753b1af3057
    +size 21809
    diff --git a/screenshots/dosbox/install-su-copy.png b/screenshots/dosbox/install-su-copy.png
    index 274dd2c1..7917893c 100644
    --- a/screenshots/dosbox/install-su-copy.png
    +++ b/screenshots/dosbox/install-su-copy.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:ce1735120c474fcaad191a6d2646b164d4b84e351e958e46f8ecfa2a39eeaaa0
    -size 17281
    +oid sha256:6d79e6eb4acb6e4d5a4ff58b05c7a49d4c2a5953be73da2814cd8f732ca5d27e
    +size 24659
    diff --git a/screenshots/dosbox/install-su-disk.png b/screenshots/dosbox/install-su-disk.png
    index 32b9b506..f475d942 100644
    --- a/screenshots/dosbox/install-su-disk.png
    +++ b/screenshots/dosbox/install-su-disk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:7f3069f7935200e9973e5902029c604c4d8f3e8e2989d2f3e608c492901ca18a
    -size 17730
    +oid sha256:80472d3fc9852f45ae4fa1b1ff29535b6586d28853fd842a585151cb95d9e78c
    +size 23422
    diff --git a/screenshots/dosbox/install-su-done.png b/screenshots/dosbox/install-su-done.png
    index 436cf1ca..65c53b03 100644
    --- a/screenshots/dosbox/install-su-done.png
    +++ b/screenshots/dosbox/install-su-done.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:82de2cdff532a7be265cf8ca9fb39d212677b393b64fbb191cdcd5674584c077
    -size 16310
    +oid sha256:9dc12906f9d8052de2f6890597bf868e4c69ceaf5e0f2b46dc1d33d5794f2b25
    +size 22957
    diff --git a/screenshots/dosbox/install-su-fold.png b/screenshots/dosbox/install-su-fold.png
    index 80af0974..10c49f77 100644
    --- a/screenshots/dosbox/install-su-fold.png
    +++ b/screenshots/dosbox/install-su-fold.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:02cefd9f80b7b54d90d5614c08c570b25a26979c2dea6e1c932058e7f5122434
    -size 16454
    +oid sha256:16570797a20a808d62efb179973f26e9f9e5b8c17de085690c149ed1d547a7b4
    +size 21789
    diff --git a/screenshots/dosbox/install-su-lic.png b/screenshots/dosbox/install-su-lic.png
    index 52dd988a..a5f80c35 100644
    --- a/screenshots/dosbox/install-su-lic.png
    +++ b/screenshots/dosbox/install-su-lic.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6082149d59c1fd4a4fa2b5333522434d2703ee0e29f653a66208d747301b5f74
    -size 17327
    +oid sha256:28003d2ec8561fcece2bec259abefa831b1b2ba1e38c446963a1872512e0315d
    +size 20521
    diff --git a/screenshots/dosbox/install-su-unin.png b/screenshots/dosbox/install-su-unin.png
    index af3047f5..9bebaef5 100644
    --- a/screenshots/dosbox/install-su-unin.png
    +++ b/screenshots/dosbox/install-su-unin.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:351484aaaa1aa48ab8f208d235677a2ccefeaaffb9ee4a12646e49e2157ccb3d
    -size 14947
    +oid sha256:4df2274a81a2dfa8864312f402d6899d72ed1669f67a884baa983a455f6dbcb3
    +size 18185
    diff --git a/screenshots/dosbox/install-su-wrong.png b/screenshots/dosbox/install-su-wrong.png
    index 6136ee81..952e4585 100644
    --- a/screenshots/dosbox/install-su-wrong.png
    +++ b/screenshots/dosbox/install-su-wrong.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:c48be5e48bd4b5890fd0ccacbd137ceac9a22efc86cfcc2cfeaeab507673b142
    -size 16944
    +oid sha256:15fc79b37a8cab0ddbe6e802857655c8445f20eeba3419e2a69725203bc9e2ba
    +size 22170
    diff --git a/screenshots/dosbox/instbase-demo.png b/screenshots/dosbox/instbase-demo.png
    new file mode 100644
    index 00000000..ce74ebd8
    --- /dev/null
    +++ b/screenshots/dosbox/instbase-demo.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:5e004e20c5fd07990538ff2c393efb75fe6f347e2d4f3503d5401988afb28885
    +size 21381
    diff --git a/screenshots/dosbox/instbase-fonts.png b/screenshots/dosbox/instbase-fonts.png
    new file mode 100644
    index 00000000..88b75706
    --- /dev/null
    +++ b/screenshots/dosbox/instbase-fonts.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:257380789d93e3dade5dd095cbcf148acc76b0dd809911bee6d3f0849fa0dc5c
    +size 19735
    diff --git a/screenshots/dosbox/instbase-su-copy.png b/screenshots/dosbox/instbase-su-copy.png
    new file mode 100644
    index 00000000..1fee6c29
    --- /dev/null
    +++ b/screenshots/dosbox/instbase-su-copy.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:fdbf49cb682467b8fd4f1f8e34fcb449fd5fff43cb0d334e1a33640ed77c0dcc
    +size 24503
    diff --git a/screenshots/dosbox/instbase-su-done.png b/screenshots/dosbox/instbase-su-done.png
    new file mode 100644
    index 00000000..65c53b03
    --- /dev/null
    +++ b/screenshots/dosbox/instbase-su-done.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:9dc12906f9d8052de2f6890597bf868e4c69ceaf5e0f2b46dc1d33d5794f2b25
    +size 22957
    diff --git a/screenshots/dosbox/instbase-su-fold.png b/screenshots/dosbox/instbase-su-fold.png
    new file mode 100644
    index 00000000..2356a6c1
    --- /dev/null
    +++ b/screenshots/dosbox/instbase-su-fold.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:c3ffe7725b830def55f89dcdfebfc292b29ddcb653374b83a52a04dc25e4f3c1
    +size 21738
    diff --git a/screenshots/dosbox/instbase-su-lic.png b/screenshots/dosbox/instbase-su-lic.png
    new file mode 100644
    index 00000000..a5f80c35
    --- /dev/null
    +++ b/screenshots/dosbox/instbase-su-lic.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:28003d2ec8561fcece2bec259abefa831b1b2ba1e38c446963a1872512e0315d
    +size 20521
    diff --git a/screenshots/dosbox/instbase-su-unin.png b/screenshots/dosbox/instbase-su-unin.png
    new file mode 100644
    index 00000000..9bebaef5
    --- /dev/null
    +++ b/screenshots/dosbox/instbase-su-unin.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:4df2274a81a2dfa8864312f402d6899d72ed1669f67a884baa983a455f6dbcb3
    +size 18185
    diff --git a/screenshots/dosbox/menus-m-design.png b/screenshots/dosbox/menus-m-design.png
    index c01e4407..917b6fca 100644
    --- a/screenshots/dosbox/menus-m-design.png
    +++ b/screenshots/dosbox/menus-m-design.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:8cdbffd36d02a2fd16b3e86d1bcb90c487f0440e30898ebddf21bb61b26861a1
    -size 23000
    +oid sha256:697d37a38da9c8c668a14d77c9b3a80345dc54df17a5377d6af8dca2b1146442
    +size 23484
    diff --git a/screenshots/dosbox/sdk-sdktbx.png b/screenshots/dosbox/sdk-sdktbx.png
    index 06535181..80120caf 100644
    --- a/screenshots/dosbox/sdk-sdktbx.png
    +++ b/screenshots/dosbox/sdk-sdktbx.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:b636eef5ae0dc8fe5c132c2b7d37c94ccdfa536f72d70b83b6cc2c0fc647d59c
    -size 22905
    +oid sha256:68680747bb40b2de6f9aaf00d8589f107318e126eba5f4ded50b3003ec3a8e64
    +size 23397
    diff --git a/screenshots/dosbox/telnet-telnet.png b/screenshots/dosbox/telnet-telnet.png
    index 3a30d0a4..52c0fc04 100644
    --- a/screenshots/dosbox/telnet-telnet.png
    +++ b/screenshots/dosbox/telnet-telnet.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:e9f77a5606a378645c0e86d2f2849cb7eccccc06efc8bdcf40fa6d2f5ab8034e
    -size 13578
    +oid sha256:b8679baf25a7213f266c44131d3549769a81e16086d89d5d28b067449c0a9ddb
    +size 13739
    diff --git a/screenshots/dosbox/ucide-ucide-ds.png b/screenshots/dosbox/ucide-ucide-ds.png
    index 06915901..70d3877e 100644
    --- a/screenshots/dosbox/ucide-ucide-ds.png
    +++ b/screenshots/dosbox/ucide-ucide-ds.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:524ccf39bc12ab0172c480795b7053cd8dcf3aae9fea34320c9a679b815315f1
    -size 31120
    +oid sha256:0979cbe27301ac2299b437d14fbf6850bad838a150c206f58fc81f673c01d5e4
    +size 31507
    diff --git a/screenshots/dosbox/ucide-ucide-tb.png b/screenshots/dosbox/ucide-ucide-tb.png
    index 94c5c616..e147e726 100644
    --- a/screenshots/dosbox/ucide-ucide-tb.png
    +++ b/screenshots/dosbox/ucide-ucide-tb.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:e0847ddd48455ad7a16b279e86aa7faaacaaa94acc563ea635aacaef53ee330a
    -size 31611
    +oid sha256:58103050f0a0174d722831a2a7771841662980826275be5a220c21816b115b94
    +size 31620
    diff --git a/screenshots/dosbox/upgrade-su-copy.png b/screenshots/dosbox/upgrade-su-copy.png
    index e41442e3..1fee6c29 100644
    --- a/screenshots/dosbox/upgrade-su-copy.png
    +++ b/screenshots/dosbox/upgrade-su-copy.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:c5a3aedf87a9d95bfe86d681049462cd11c66bb82e634088bdcb0d907e4bea38
    -size 17127
    +oid sha256:fdbf49cb682467b8fd4f1f8e34fcb449fd5fff43cb0d334e1a33640ed77c0dcc
    +size 24503
    diff --git a/screenshots/dosbox/upgrade-su-done.png b/screenshots/dosbox/upgrade-su-done.png
    index 436cf1ca..65c53b03 100644
    --- a/screenshots/dosbox/upgrade-su-done.png
    +++ b/screenshots/dosbox/upgrade-su-done.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:82de2cdff532a7be265cf8ca9fb39d212677b393b64fbb191cdcd5674584c077
    -size 16310
    +oid sha256:9dc12906f9d8052de2f6890597bf868e4c69ceaf5e0f2b46dc1d33d5794f2b25
    +size 22957
    diff --git a/screenshots/dosbox/upgrade-su-fold.png b/screenshots/dosbox/upgrade-su-fold.png
    index 0db61b4a..10c49f77 100644
    --- a/screenshots/dosbox/upgrade-su-fold.png
    +++ b/screenshots/dosbox/upgrade-su-fold.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:a4a14649ab3066c4108c685dc6bb8b78b5be9f5c507eb18c3099c18311baaff3
    -size 16474
    +oid sha256:16570797a20a808d62efb179973f26e9f9e5b8c17de085690c149ed1d547a7b4
    +size 21789
    diff --git a/screenshots/dosbox/upgrade-su-lic.png b/screenshots/dosbox/upgrade-su-lic.png
    index 52dd988a..a5f80c35 100644
    --- a/screenshots/dosbox/upgrade-su-lic.png
    +++ b/screenshots/dosbox/upgrade-su-lic.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6082149d59c1fd4a4fa2b5333522434d2703ee0e29f653a66208d747301b5f74
    -size 17327
    +oid sha256:28003d2ec8561fcece2bec259abefa831b1b2ba1e38c446963a1872512e0315d
    +size 20521
    diff --git a/screenshots/dosbox/upgrade-su-unin.png b/screenshots/dosbox/upgrade-su-unin.png
    index af3047f5..9bebaef5 100644
    --- a/screenshots/dosbox/upgrade-su-unin.png
    +++ b/screenshots/dosbox/upgrade-su-unin.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:351484aaaa1aa48ab8f208d235677a2ccefeaaffb9ee4a12646e49e2157ccb3d
    -size 14947
    +oid sha256:4df2274a81a2dfa8864312f402d6899d72ed1669f67a884baa983a455f6dbcb3
    +size 18185
    diff --git a/screenshots/dosbox/upgrade-su-upgr.png b/screenshots/dosbox/upgrade-su-upgr.png
    index 55bc5afa..83ee1500 100644
    --- a/screenshots/dosbox/upgrade-su-upgr.png
    +++ b/screenshots/dosbox/upgrade-su-upgr.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:1030284051814c5abadf8babc30a9d92671cf0c68bdf17d4287c8cf5a0c92363
    -size 16882
    +oid sha256:e0a8f8c12e567eabcf2dc238b375401a4a7db77e1a868c3bba0397e0e752c8e8
    +size 22144
    diff --git a/screenshots/dosbox/video-v-800.png b/screenshots/dosbox/video-v-800.png
    index bbc92f67..2c09a4c4 100644
    --- a/screenshots/dosbox/video-v-800.png
    +++ b/screenshots/dosbox/video-v-800.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:682c2a4eadf7c84bfaff80a564ea2cd6b89b32d8e2648f64b94c040dff2ea9a3
    -size 27054
    +oid sha256:98fed6f84d56c4efd88b87ed411c7813fd89afd4c15e2b809d8ff88a4dc31d5a
    +size 26611
    diff --git a/screenshots/dosbox/video-v-pick.png b/screenshots/dosbox/video-v-pick.png
    index 6b939fa9..3741c781 100644
    --- a/screenshots/dosbox/video-v-pick.png
    +++ b/screenshots/dosbox/video-v-pick.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:1c4e224d31db97ff099dc9cc0d268388419ebe8f3d131f1f1366cd100acecca4
    -size 20301
    +oid sha256:9a3398896c493865e57eb7dc7e7f16f6b60fb20970d4bf42000d40c9fd8ed875
    +size 20283
    diff --git a/screenshots/dosbox/welcome-wl-help.png b/screenshots/dosbox/welcome-wl-help.png
    index b772f1b0..8518787f 100644
    --- a/screenshots/dosbox/welcome-wl-help.png
    +++ b/screenshots/dosbox/welcome-wl-help.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:242657f05cc4b6fc531ecef24f6e2639e169675371f282af275973cc9ebdb38e
    -size 18913
    +oid sha256:b1b3758c817e427e44cbc91837a48f04d88548435da0854d27dded272235584c
    +size 18884
    diff --git a/screenshots/dosbox/welcome-wl-open.png b/screenshots/dosbox/welcome-wl-open.png
    index abd2ab8f..8fca1d66 100644
    --- a/screenshots/dosbox/welcome-wl-open.png
    +++ b/screenshots/dosbox/welcome-wl-open.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6ad65605b7b0d10a2fa4ce1e59a6255b1667dfef8a53fd1be0efb7e7a1f3957f
    -size 19200
    +oid sha256:84fe52aab2f648ad623509b2be7e39ff5d5809680566e1aaf6f14753b1af3057
    +size 21809
    diff --git a/screenshots/dosbox/welcome-wl-tour.png b/screenshots/dosbox/welcome-wl-tour.png
    index a772093c..ff0fbc0a 100644
    --- a/screenshots/dosbox/welcome-wl-tour.png
    +++ b/screenshots/dosbox/welcome-wl-tour.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:7a0e03692d92fbc8b95a858c72e7fb83c169bf8f19b4c5462be5f4ed6e5dff08
    -size 18342
    +oid sha256:c3eda8d3a118877e4417c8c9ed52fd4530e6cab356482ff0d8ce6cdc8cdeece1
    +size 24645
    diff --git a/screenshots/dosbox/welcome-wl-tour2.png b/screenshots/dosbox/welcome-wl-tour2.png
    index d9f17e84..e9c8d62f 100644
    --- a/screenshots/dosbox/welcome-wl-tour2.png
    +++ b/screenshots/dosbox/welcome-wl-tour2.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:c63e835f1d7da0b190771c96b32c7dd532e19e3dd741ac25218361ab0055720b
    -size 18588
    +oid sha256:858c116aa5e55367589908c9f0c3dea6cf3adf0174d7f3876eb306c45db7a261
    +size 24465
    diff --git a/screenshots/masks/f1-h-filemg.mask b/screenshots/masks/f1-h-filemg.mask
    index 388d7c69..0146c2a3 100644
    --- a/screenshots/masks/f1-h-filemg.mask
    +++ b/screenshots/masks/f1-h-filemg.mask
    @@ -1,2 +1,2 @@
     # File Manager file times showing to the right of the help window
    -608 150 32 160
    +608 150 32 176
    diff --git a/screenshots/masks/fonts-edit.mask b/screenshots/masks/fonts-edit.mask
    new file mode 100644
    index 00000000..bf21d0c0
    --- /dev/null
    +++ b/screenshots/masks/fonts-edit.mask
    @@ -0,0 +1,3 @@
    +# The RichEdit's blinking caret (after the bolded line): on or off
    +# depends on timing
    +82 136 5 24
    diff --git a/screenshots/masks/fonts8-edit.mask b/screenshots/masks/fonts8-edit.mask
    new file mode 100644
    index 00000000..bf21d0c0
    --- /dev/null
    +++ b/screenshots/masks/fonts8-edit.mask
    @@ -0,0 +1,3 @@
    +# The RichEdit's blinking caret (after the bolded line): on or off
    +# depends on timing
    +82 136 5 24
    diff --git a/screenshots/masks/instbase-su-fold.mask b/screenshots/masks/instbase-su-fold.mask
    new file mode 100644
    index 00000000..9097904a
    --- /dev/null
    +++ b/screenshots/masks/instbase-su-fold.mask
    @@ -0,0 +1,4 @@
    +# Setup's "DVX needs about N MB" figure (grows with the package) and the rest of that line
    +202 163 176 16
    +# Blinking text caret after C:\DVX in the folder box
    +123 211 3 18
    diff --git a/screenshots/masks/pardsgn-pardsgn.mask b/screenshots/masks/pardsgn-pardsgn.mask
    new file mode 100644
    index 00000000..62a64f85
    --- /dev/null
    +++ b/screenshots/masks/pardsgn-pardsgn.mask
    @@ -0,0 +1,3 @@
    +# The code editor's blinking caret (after "Start"): on or off depends on
    +# timing
    +236 120 4 20
    diff --git a/src/apps/kpunch/Makefile b/src/apps/kpunch/Makefile
    index 0b9a74ce..948fce5a 100644
    --- a/src/apps/kpunch/Makefile
    +++ b/src/apps/kpunch/Makefile
    @@ -33,6 +33,7 @@ OBJDIR  = ../../../obj/apps
     BINROOT = ../../../bin
     BINDIR  = $(BINROOT)/apps
     DVXRES = ../../../bin/host/dvxres
    +MKFONT = ../../../bin/host/mkfont
     
     # C apps are discovered, never listed: every directory / holding
     # /.c is one app.  All .c files in that directory are linked into
    @@ -79,7 +80,15 @@ $(BASCOMP): | dvxbasic
     appSrcs = $(wildcard $(1)/*.c)
     appObjs = $(patsubst $(1)/%.c,$(OBJDIR)/$(1)/%.o,$(call appSrcs,$(1)))
     appRes  = $(wildcard $(1)/$(1).res)
    -appDeps = $(call appRes,$(1)) $(if $(call appRes,$(1)),$(wildcard $(1)/*.bmp))
    +
    +# A program's own fonts: /fonts.ini, when present, made by mkfont
    +# into $(OBJDIR)//fonts before the program is built (a C program's
    +# .res names them as font resources, a BASIC program's .dbp in [Fonts]);
    +# the stamp stands for the set.
    +appFontIni = $(wildcard $(1)/fonts.ini)
    +appFonts   = $(if $(call appFontIni,$(1)),$(OBJDIR)/$(1)/fonts.stamp)
    +
    +appDeps = $(call appRes,$(1)) $(if $(call appRes,$(1)),$(wildcard $(1)/*.bmp)) $(call appFonts,$(1))
     
     # Data files a program installs beside itself, C or BASIC alike:
     # APP_DATA_ lists them ( being the last part of the program's
    @@ -115,7 +124,17 @@ $(OBJDIR)/$(1)/%.o: $(1)/%.c $$(DVX_MAKEFILES)
     	$$(CC) $$(CFLAGS) -c -o $$@ $$<
     endef
     
    +# $(1) = app directory name (one with a fonts.ini).
    +define APP_FONT_RULES
    +$(OBJDIR)/$(1)/fonts.stamp: $(1)/fonts.ini $$(MKFONT)
    +	rm -rf $(OBJDIR)/$(1)/fonts $$@
    +	@mkdir -p $(OBJDIR)/$(1)
    +	$$(MKFONT) $(1)/fonts.ini $$(FONT_SOURCES) $(OBJDIR)/$(1)/fonts
    +	touch $$@
    +endef
    +
     $(foreach a,$(C_APPS),$(eval $(call C_APP_RULES,$(a))))
    +$(foreach a,$(C_APPS),$(if $(call appFontIni,$(a)),$(eval $(call APP_FONT_RULES,$(a)))))
     $(foreach a,$(C_APPS),$(if $(APP_DATA_$(a)),$(eval $(call APP_DATA_RULES,$(a),$(a),$(a)))))
     
     C_APP_OBJS = $(foreach a,$(C_APPS),$(call appObjs,$(a)))
    @@ -130,12 +149,13 @@ C_APP_OBJS = $(foreach a,$(C_APPS),$(call appObjs,$(a)))
     define BASIC_APP_RULES
     $(call basicName,$(1)): $(call basicApp,$(1))
     
    -$(call basicApp,$(1)): $(call basicDeps,$(call basicDir,$(1))) $$(BASCOMP) | dvxbasic
    +$(call basicApp,$(1)): $(call basicDeps,$(call basicDir,$(1))) $(call appFonts,$(call basicDir,$(1))) $$(BASCOMP) | dvxbasic
     	@mkdir -p $$(dir $$@)
     	$$(BASCOMP) $(call basicDir,$(1))/$(call basicName,$(1)).dbp -o $$@ -release
     endef
     
     $(foreach a,$(BASIC_APPS),$(eval $(call BASIC_APP_RULES,$(a))))
    +$(foreach a,$(BASIC_APPS),$(if $(call appFontIni,$(call basicDir,$(a))),$(eval $(call APP_FONT_RULES,$(call basicDir,$(a))))))
     $(foreach a,$(BASIC_APPS),$(if $(APP_DATA_$(call basicName,$(a))),$(eval $(call APP_DATA_RULES,$(call basicDir,$(a)),$(call basicOut,$(a)),$(call basicName,$(a))))))
     
     # Native host build (test harness): each C app becomes a plain ELF shared
    diff --git a/src/apps/kpunch/README.md b/src/apps/kpunch/README.md
    index c95be71f..bf0e590c 100644
    --- a/src/apps/kpunch/README.md
    +++ b/src/apps/kpunch/README.md
    @@ -156,7 +156,7 @@ individual apps.
       `.dbp`, where `` is the name of the `.app` file.
     * The top-level `compile-help` target runs `src/tools/helpFiles.sh`,
       which finds every such file by name, compiles it to `.hlp`
    -  next to `.app`, deploys the source as `.DHS` and writes an
    +  next to `.app`, deploys the source as `.DHS`, and writes an
       `.HCF` so the loader's boot-time help rebuild produces it too.
       No list needs editing when an app is added.
     * F1 needs no code: before `appMain` the shell sets `ctx->helpFile` to
    @@ -182,15 +182,15 @@ See `claudeDocs/perAppHelp.md` for the full rules.
     | Multi-instance | No |
     
     The desktop: program groups in a list on the left, the selected group's
    -items as large icons, small icons or a list on the right, a search box
    +items as large icons, small icons, or a list on the right, a search box
     that filters every group, and a status bar.  The automatic "Installed
     Programs" group is rebuilt from `shellProgramScan()` at start, on
    -View > Refresh and when the window comes to the front with programs
    +View > Refresh, and when the window comes to the front with programs
     added or removed; items in the "Startup" group run once DVX is up.
     Groups, items (name, program, arguments, working folder, icon, shortcut
    -key, run minimized, grid cell), view, options and Run history are saved
    +key, run minimized, grid cell), view, options, and Run history are saved
     in `CONFIG/APPS/KPUNCH/PROGMAN/PROGMAN.INI` after every change (crash-safe
    -rewrite).  Running, the Run dialog, program discovery and file
    +rewrite).  Running, the Run dialog, program discovery, and file
     associations are shell library services (`shellRun.h`, `shellProg.h`,
     `shellAssoc.h`) shared with other programs.  Closing it exits DVX
     through `shellRequestExit()`; `shellRegisterDesktopUpdate()` keeps the
    @@ -253,14 +253,14 @@ Guide" opens the system help. Built from `welcome.dbp` +
     | Type | BASIC (main-loop via basstub) |
     | Multi-instance | Yes |
     
    -Drive buttons, a path box, a folder tree and a sortable file list
    +Drive buttons, a path box, a folder tree, and a sortable file list
     with open / run, copy, move, cut / copy / paste between windows,
    -rename, delete, new folder, properties (attributes), selection and
    -filter commands. Copy, move and delete run as jobs with their own
    +rename, delete, new folder, properties (attributes), selection, and
    +filter commands. Copy, move, and delete run as jobs with their own
     progress windows, driven by a Timer, so the program stays usable.
     Built from `fileMgr.dbp`: `fileMgr.frm` (the window), `fmProps.frm`
     (Properties), `fileMgr.bas` (the job engine), `fmJob.cls`, with the
    -fileops.bas, shell.bas, commdlg.bas and help.bas libraries; its help
    +fileops.bas, shell.bas, commdlg.bas, and help.bas libraries; its help
     is `fileMgr.dhs`.
     
     ### Terminal (terminal, BASIC)
    @@ -273,12 +273,12 @@ is `fileMgr.dhs`.
     
     A BBS / serial terminal: serial port, modem (dialing, redial, carrier
     loss), the secure link to the DVX host proxy (with or without a
    -passphrase) and Telnet; a phone book of sessions (TERMINAL.INI in its
    +passphrase), and Telnet; a phone book of sessions (TERMINAL.INI in its
     settings folder); the ANSI terminal with scrollback, colors, local echo,
    -answerback, capture to file and break; XMODEM, XMODEM-1K, YMODEM and
    +answerback, capture to file, and break; XMODEM, XMODEM-1K, YMODEM, and
     ZMODEM uploads and downloads (ZMODEM downloads start by themselves)
     with a progress window that asks about existing files. The connection,
    -the terminal feed and the transfers run in the conn and xfer libraries
    +the terminal feed, and the transfers run in the conn and xfer libraries
     (conn.bas, xfer.bas); a 10 ms Timer moves them along. Started with
     `telnet://host[:port]` it connects to that host. Built from
     `terminal.dbp`: `terminal.frm` (the window), `termBook.frm` (Phone
    @@ -286,7 +286,7 @@ Book), `termSess.frm` (Session Settings), `termXfer.frm` (Transfer
     Options), `termPass.frm` (passphrase), `termProg.frm` (File Transfer),
     `terminal.bas` (the phone book and shared state), `termMain.bas` (the
     start), with the conn.bas, xfer.bas, prefs.bas, fileops.bas, shell.bas,
    -commdlg.bas and help.bas libraries; its help is `terminal.dhs`.
    +commdlg.bas, and help.bas libraries; its help is `terminal.dhs`.
     
     ### DVX Demo (dvxdemo)
     
    @@ -325,7 +325,7 @@ System configuration with five tabs:
       and save
     * **Desktop** -- wallpaper image and display mode
     * **Video** -- resolution and colour depth switching
    -* **File Types** -- file associations (`cpTypes.c`): add, change and
    +* **File Types** -- file associations (`cpTypes.c`): add, change, and
       remove which program opens each extension
     
     Changes preview live. OK writes to `CONFIG/DVX.INI` and the file types
    @@ -388,7 +388,7 @@ BASIC program uses through `hlpfile.bas` and HelpPane.
     | Type | BASIC (main-loop via basstub) |
     | Multi-instance | No |
     
    -Dot-by-dot editor for 16x16, 24x24 and 32x32 icons. Pencil /
    +Dot-by-dot editor for 16x16, 24x24, and 32x32 icons. Pencil /
     eraser / colour picker tools, 32-colour palette, BMP or PNG open,
     PNG (or BMP by extension) save.
     
    @@ -543,9 +543,9 @@ dependencies, and per-app crash diagnostics.
     ## Files
     
     Each app's own help source `.dhs` sits beside its `.c` or `.dbp`
    -(see Help above). `progman/`, `dvxbasic/` and `dvxhelp/` also hold
    +(see Help above). `progman/`, `dvxbasic/`, and `dvxhelp/` also hold
     help configured by hand in the top-level Makefile and their `.hcf`
    -files: the system help (`progman/userHelp.dhs`), the BASIC help and the
    +files: the system help (`progman/userHelp.dhs`), the BASIC help, and the
     viewer's help.
     
     ```
    diff --git a/src/apps/kpunch/basdemo/basdemo.dbp b/src/apps/kpunch/basdemo/basdemo.dbp
    index 3f047df4..1f018641 100644
    --- a/src/apps/kpunch/basdemo/basdemo.dbp
    +++ b/src/apps/kpunch/basdemo/basdemo.dbp
    @@ -11,7 +11,8 @@ Icon = ICON32.BMP
     File0 = ../../../include/basic/commdlg.bas
     File1 = ../../../include/basic/fileops.bas
     File2 = ../../../include/basic/help.bas
    -Count = 3
    +File3 = ../../../include/basic/font.bas
    +Count = 4
     
     [Classes]
     File0 = shape.cls
    diff --git a/src/apps/kpunch/basdemo/basdemo.dhs b/src/apps/kpunch/basdemo/basdemo.dhs
    index ec44f306..7ca728eb 100644
    --- a/src/apps/kpunch/basdemo/basdemo.dhs
    +++ b/src/apps/kpunch/basdemo/basdemo.dhs
    @@ -42,19 +42,19 @@ The buttons in the Language Demonstrations box each run a short example. The res
     .table
       Button          What it shows
       ------          -------------
    -  Types           The kinds of values BASIC works with: whole numbers, decimals, text and true/false.
    +  Types           The kinds of values BASIC works with: whole numbers, decimals, text, and true/false.
       Math            Arithmetic and math functions such as square roots.
    -  Strings         Working with text: cutting, joining, searching and changing case.
    +  Strings         Working with text: cutting, joining, searching, and changing case.
       Arrays          Lists and tables of values.
       DATA/READ       Values stored inside the program and read back.
       Control Flow    Loops and decisions.
       UDT             Records that group several values together.
       Optional        Procedures with optional parts.
       Errors          Catching a mistake (dividing by zero) without stopping the program.
    -  Format          Formatting numbers as money, percentages and so on.
    -  File I/O        Writing a small file, reading it back and deleting it.
    -  System          Where the program is stored, the date, the time and similar details.
    -  INI             Writing a small settings file, reading it back and deleting it.
    +  Format          Formatting numbers as money, percentages, and so on.
    +  File I/O        Writing a small file, reading it back, and deleting it.
    +  System          Where the program is stored, the date, the time, and similar details.
    +  INI             Writing a small settings file, reading it back, and deleting it.
       Dialogs         Message boxes and boxes that ask you to type or pick something.
       Clear           Empties the text area.
     .endtable
    @@ -73,7 +73,8 @@ The File I/O and INI examples create their files in the program's own data folde
       Demos   Graphics...            Open a window with a drawing. Draw paints shapes and text; Clear wipes it.
       Demos   Dynamic Form...        Open a window that the program builds while running, with a button that counts.
       Demos   Timer...               Open a window with a counter that ticks twice a second.
    -  Demos   Objects                Make shapes from the Shape class module, keep them in a Collection and list them with FOR EACH.
    +  Demos   Fonts...               Open a window of text in several fonts, styles, and colors; a sample line whose font you choose with Choose Font; and a box to write styled text in, with Bold, Italic, and Underline buttons.
    +  Demos   Objects                Make shapes from the Shape class module, keep them in a Collection, and list them with FOR EACH.
       Help    Contents               Open this help.
       Help    About...               Show the program name and copyright.
     .endtable
    diff --git a/src/apps/kpunch/basdemo/basdemo.frm b/src/apps/kpunch/basdemo/basdemo.frm
    index 26f68e48..61e9137b 100644
    --- a/src/apps/kpunch/basdemo/basdemo.frm
    +++ b/src/apps/kpunch/basdemo/basdemo.frm
    @@ -42,6 +42,9 @@ Begin Form BasicDemo
             Begin Menu mnuTimer
                 Caption = "&Timer..."
             End
    +        Begin Menu mnuFonts
    +            Caption = "&Fonts..."
    +        End
             Begin Menu mnuObjects
                 Caption = "&Objects"
             End
    @@ -198,18 +201,25 @@ CONST GFX_TEXT_Y     = 200
     CONST GFX_TEXT_LINE  = 20
     CONST PATH_SHOW_LEN  = 40
     CONST HELP_FILE      = "basdemo.hlp"
    +CONST FONT_FORM_W    = 420
    +CONST FONT_FORM_H    = 380
    +CONST FONT_RICH_W    = 360
    +CONST FONT_BIG       = 20
    +CONST FONT_SERIF     = "DejaVu Serif"
     
     DIM SHARED gfxOpen   AS BOOLEAN
     DIM SHARED dynOpen   AS BOOLEAN
     DIM SHARED dynCount  AS INTEGER
     DIM SHARED timerOpen AS BOOLEAN
     DIM SHARED tickCount AS LONG
    +DIM SHARED fontOpen  AS BOOLEAN
     
     gfxOpen   = False
     dynOpen   = False
     dynCount  = 0
     timerOpen = False
     tickCount = 0
    +fontOpen  = False
     
     Load BasicDemo
     BasicDemo.Show
    @@ -218,7 +228,7 @@ OutArea.SetShowLineNumbers False
     OutArea.SetReadOnly True
     Say "Welcome to the DVX BASIC Feature Tour!"
     Say "Each button below runs a self-contained example."
    -Say "Check the Demos menu for graphics, dynamic UI, and timer demos."
    +Say "Check the Demos menu for graphics, dynamic UI, timer, and font demos."
     Say ""
     
     
    @@ -947,6 +957,129 @@ SUB TimerForm_Unload
     END SUB
     
     
    +SUB mnuFonts_Click
    +    ' Fonts: a RichLabel's runs in fonts, styles, and colors of their own,
    +    ' a label whose font the Font dialog picks (commdlg.bas), the
    +    ' installed families (font.bas), and a RichEdit with style buttons
    +    IF fontOpen THEN
    +        EXIT SUB
    +    END IF
    +
    +    DIM frm AS LONG
    +    SET frm = CreateForm("FontForm", FONT_FORM_W, FONT_FORM_H)
    +    frm.Caption = "Fonts"
    +    fontOpen = True
    +
    +    DIM rich AS LONG
    +    SET rich = CreateControl(frm, "RichLabel", "FontRich")
    +    FontRich.Width = FONT_RICH_W
    +    FontRich.AddText "Text in the control's own font, "
    +    FontRich.SetStyle FONT_BOLD
    +    FontRich.AddText "bold"
    +    FontRich.SetStyle FONT_ITALIC
    +    FontRich.AddText ", italic"
    +    FontRich.SetStyle FONT_UNDERLINE
    +    FontRich.AddText ", underlined"
    +    FontRich.SetStyle 0
    +    FontRich.AddText " and "
    +    FontRich.SetColor RGB(192, 0, 0)
    +    FontRich.AddText "in color"
    +    FontRich.SetColor -1
    +    FontRich.AddText "."
    +    FontRich.SetFont FONT_SERIF, FONT_BIG
    +    FontRich.AddParagraph 1
    +    FontRich.AddText "A centered paragraph in a bigger serif"
    +
    +    DIM lbl AS LONG
    +    SET lbl = CreateControl(frm, "Label", "FontSample")
    +    FontSample.Caption = "The quick brown fox jumps over the lazy dog."
    +
    +    DIM info AS LONG
    +    SET info = CreateControl(frm, "Label", "FontInfo")
    +    FontInfo.Caption = CSTR(FontFamilyCount()) + " font families are installed."
    +
    +    DIM btns AS LONG
    +    SET btns = CreateControl(frm, "HBox", "FontBtns")
    +
    +    DIM bPick AS LONG
    +    SET bPick = CreateControl(frm, "CommandButton", "BFontPick", btns)
    +    BFontPick.Caption = "&Choose Font..."
    +    SetEvent bPick, "Click", "FontPick"
    +
    +    DIM bBye AS LONG
    +    SET bBye = CreateControl(frm, "CommandButton", "BFontBye", btns)
    +    BFontBye.Caption = "Close"
    +    SetEvent bBye, "Click", "FontBye"
    +
    +    DIM styles AS LONG
    +    SET styles = CreateControl(frm, "HBox", "FontStyles")
    +
    +    DIM bBold AS LONG
    +    SET bBold = CreateControl(frm, "CommandButton", "BFontBold", styles)
    +    BFontBold.Caption = "&Bold"
    +    SetEvent bBold, "Click", "FontBold"
    +
    +    DIM bItalic AS LONG
    +    SET bItalic = CreateControl(frm, "CommandButton", "BFontItalic", styles)
    +    BFontItalic.Caption = "&Italic"
    +    SetEvent bItalic, "Click", "FontItalic"
    +
    +    DIM bUnder AS LONG
    +    SET bUnder = CreateControl(frm, "CommandButton", "BFontUnder", styles)
    +    BFontUnder.Caption = "&Underline"
    +    SetEvent bUnder, "Click", "FontUnder"
    +
    +    DIM ed AS LONG
    +    SET ed = CreateControl(frm, "RichEdit", "FontEdit")
    +    FontEdit.Text = "Select some text, then click Bold, Italic, or Underline."
    +
    +    frm.Show
    +END SUB
    +
    +
    +SUB FontBold
    +    FontEdit.SelBold = NOT FontEdit.SelBold
    +END SUB
    +
    +
    +SUB FontItalic
    +    FontEdit.SelItalic = NOT FontEdit.SelItalic
    +END SUB
    +
    +
    +SUB FontUnder
    +    FontEdit.SelUnderline = NOT FontEdit.SelUnderline
    +END SUB
    +
    +
    +SUB FontPick
    +    ' The dialog starts at the sample's font; OK gives it the one chosen
    +    DIM style AS LONG
    +    style = IIf(FontSample.FontBold, FONT_BOLD, 0) OR IIf(FontSample.FontItalic, FONT_ITALIC, 0) OR IIf(FontSample.FontUnderline, FONT_UNDERLINE, 0) OR IIf(FontSample.FontStrikethru, FONT_STRIKE, 0)
    +
    +    IF DlgFont("Choose a Font", FontSample.FontName, FontSample.FontSize, style, 0) THEN
    +        style = DlgFontStyle()
    +        FontSample.FontName       = DlgFontName$()
    +        FontSample.FontSize       = DlgFontSize()
    +        FontSample.FontBold       = (style AND FONT_BOLD) <> 0
    +        FontSample.FontItalic     = (style AND FONT_ITALIC) <> 0
    +        FontSample.FontUnderline  = (style AND FONT_UNDERLINE) <> 0
    +        FontSample.FontStrikethru = (style AND FONT_STRIKE) <> 0
    +        FontInfo.Caption = FontSample.FontName + "," + STR$(FontSample.FontSize) + " pixels"
    +    END IF
    +END SUB
    +
    +
    +SUB FontBye
    +    Unload FontForm
    +END SUB
    +
    +
    +SUB FontForm_Unload
    +    fontOpen = False
    +END SUB
    +
    +
     SUB mnuObjects_Click
         ' Objects: the Shape class module, NEW, a Collection and FOR EACH
         Header "Objects"
    diff --git a/src/apps/kpunch/clock/clock.dhs b/src/apps/kpunch/clock/clock.dhs
    index 1aee677b..693db0ee 100644
    --- a/src/apps/kpunch/clock/clock.dhs
    +++ b/src/apps/kpunch/clock/clock.dhs
    @@ -37,8 +37,8 @@
     The Clock shows the current time and today's date in a small window. It updates every second.
     
     .list
    -.item The time is shown with hours, minutes and seconds, in 12-hour style with AM or PM, for example 3:07:42 PM.
    -.item The date is shown as year, month and day, for example 2026-09-27.
    +.item The time is shown with hours, minutes, and seconds, in 12-hour style with AM or PM, for example 3:07:42 PM.
    +.item The date is shown as year, month, and day, for example 2026-09-27.
     .endlist
     
     The Clock takes the time and date from your computer. If they are wrong, set them with the DOS TIME and DATE commands before you start DVX.
    @@ -49,7 +49,7 @@ You can open the Clock more than once. The first Clock opens near the top right
     
     .h2 The Clock Window
     
    -The Clock window has a fixed size; it cannot be made bigger or smaller, but you can move it, minimize it or close it like any other window. Closing the window ends that Clock.
    +The Clock window has a fixed size; it cannot be made bigger or smaller, but you can move it, minimize it, or close it like any other window. Closing the window ends that Clock.
     
     .h2 Menus
     
    diff --git a/src/apps/kpunch/cpanel/cpanel.c b/src/apps/kpunch/cpanel/cpanel.c
    index da451c68..f87d2ed5 100644
    --- a/src/apps/kpunch/cpanel/cpanel.c
    +++ b/src/apps/kpunch/cpanel/cpanel.c
    @@ -44,6 +44,7 @@
     #include "box/box.h"
     #include "button/button.h"
     #include "canvas/canvas.h"
    +#include "checkbox/checkbox.h"
     #include "dropdown/dropdown.h"
     #include "label/label.h"
     #include "listBox/listBox.h"
    @@ -144,8 +145,8 @@ static DxeAppContextT *sCtx   = NULL;
     static WindowT        *sWin   = NULL;
     static PrefsHandleT   *sPrefs = NULL;
     
    -// Saved state for Cancel: colors, mouse, desktop and video mode in the
    -// DVX.INI layout (prefsCreate; NULL when out of memory)
    +// Saved state for Cancel: colors, mouse, desktop, video mode and text
    +// smoothing in the DVX.INI layout (prefsCreate; NULL when out of memory)
     static PrefsHandleT *sSnapshot = NULL;
     
     // Mouse tab widgets
    @@ -179,6 +180,7 @@ static int32_t               sVideoCount                 = 0;
     static const char          **sVideoLabels                = NULL;  // stb_ds dynamic array
     static char                (*sLabelBufs)[CP_LABEL_BUF]   = NULL;  // stb_ds dynamic array of mode labels
     static WidgetT              *sVideoList                  = NULL;
    +static WidgetT              *sSmoothChk                  = NULL;
     static int32_t               sVideoConfirmResult         = -1;
     
     // Theme list
    @@ -212,6 +214,7 @@ static void        onMenu(WindowT *win, int32_t menuId);
     static void        onOk(WidgetT *w);
     static void        onResetColors(WidgetT *w);
     static void        onSaveTheme(WidgetT *w);
    +static void        onSmoothText(WidgetT *w);
     static void        onSpeedSlider(WidgetT *w);
     static void        onVideoApply(WidgetT *w);
     static void        onVideoConfirmClose(WindowT *win);
    @@ -580,6 +583,10 @@ static void buildVideoTab(WidgetT *page) {
         WidgetT *applyBtn = wgtButton(page, "Apply Mode");
         applyBtn->onClick = onVideoApply;
         applyBtn->prefW   = wgtPixels(CP_APPLY_BTN_W);
    +
    +    sSmoothChk = wgtCheckbox(page, "Smooth the edges of text (more than 256 colors)");
    +    wgtCheckboxSetChecked(sSmoothChk, sAc->fontSmoothing);
    +    sSmoothChk->onChange = onSmoothText;
     }
     
     
    @@ -741,7 +748,7 @@ static void onMenu(WindowT *win, int32_t menuId) {
                 break;
     
             case CMD_HELP_ABOUT:
    -            dvxInfoBox(sAc, "About Control Panel", "Control Panel\n\nChanges the mouse, colors, desktop picture, screen mode, which programs open each type of file, the network settings, the printer, the game controllers and the sounds.\n\nCopyright 2026 Scott Duensing");
    +            dvxInfoBox(sAc, "About Control Panel", "Control Panel\n\nChanges the mouse, colors, desktop picture, screen mode, which programs open each type of file, the network settings, the printer, the game controllers, and the sounds.\n\nCopyright 2026 Scott Duensing");
                 break;
     
             default:
    @@ -771,6 +778,7 @@ static void onOk(WidgetT *w) {
         dvxMouseToPrefs(sAc, sPrefs);
         dvxColorsToPrefs(sAc, sPrefs);
         dvxWallpaperToPrefs(sAc, sPrefs);
    +    dvxFontsToPrefs(sAc, sPrefs);
         prefsSetVideoMode(sPrefs, sAc->display.width, sAc->display.height, sAc->display.format.bitsPerPixel);
     
         cpNetStore(sPrefs);
    @@ -827,6 +835,13 @@ static void onSaveTheme(WidgetT *w) {
     }
     
     
    +// Applied at once, like the mouse settings (Cancel puts it back)
    +static void onSmoothText(WidgetT *w) {
    +    (void)w;
    +    dvxSetFontSmoothing(sAc, wgtCheckboxIsChecked(sSmoothChk));
    +}
    +
    +
     static void onSpeedSlider(WidgetT *w) {
         (void)w;
         updateSpeedLabel();
    @@ -998,6 +1013,7 @@ static void restoreSnapshot(void) {
         }
     
         dvxWallpaperFromPrefs(sAc, sSnapshot);
    +    dvxFontsFromPrefs(sAc, sSnapshot);
     }
     
     
    @@ -1010,6 +1026,7 @@ static void saveSnapshot(void) {
             dvxColorsToPrefs(sAc, sSnapshot);
             dvxMouseToPrefs(sAc, sSnapshot);
             dvxWallpaperToPrefs(sAc, sSnapshot);
    +        dvxFontsToPrefs(sAc, sSnapshot);
             prefsSetVideoMode(sSnapshot, sAc->display.width, sAc->display.height, sAc->display.format.bitsPerPixel);
         }
     }
    diff --git a/src/apps/kpunch/cpanel/cpanel.dhs b/src/apps/kpunch/cpanel/cpanel.dhs
    index 4fe2600c..9a42a8ba 100644
    --- a/src/apps/kpunch/cpanel/cpanel.dhs
    +++ b/src/apps/kpunch/cpanel/cpanel.dhs
    @@ -34,12 +34,12 @@
     
     .h1 Control Panel
     
    -The Control Panel changes how DVX looks and works: the mouse, the screen colors, the desktop picture, the screen resolution, which program opens each kind of file, the network settings, the printer, the game controllers and the sounds. Its window has nine tabs; when they do not all fit, the arrows beside them show the others. Click a tab to see its settings.
    +The Control Panel changes how DVX looks and works: the mouse, the screen colors, the desktop picture, the screen resolution, which program opens each kind of file, the network settings, the printer, the game controllers, and the sounds. Its window has nine tabs; when they do not all fit, the arrows beside them show the others. Click a tab to see its settings.
     
    -.link cpanel.mouse Mouse: pointer speed, double-click speed and the scroll wheel
    +.link cpanel.mouse Mouse: pointer speed, double-click speed, and the scroll wheel
     .link cpanel.colors Colors: the colors of every part of the screen, and color themes
     .link cpanel.desktop Desktop: the picture behind the windows
    -.link cpanel.video Video: the screen resolution and number of colors
    +.link cpanel.video Video: the screen resolution, number of colors, and text smoothing
     .link cpanel.types File Types: which program opens each kind of document
     .link cpanel.network Network: connecting DVX to a network, and its status
     .link cpanel.printer Printer: your printer's model, where it is connected, and the page setup
    @@ -84,7 +84,7 @@ The one exception is a theme saved with Save As... on the Colors tab: the theme
       Scroll Wheel         Normal or Reversed. Reversed makes the wheel scroll the other way.
       Scroll Speed         How many lines each click of the wheel scrolls, from 1 to 10. The number is shown at the right.
       Double-Click Speed   How quickly you must click twice for a double-click. Toward Slow gives you more time. The time in milliseconds is shown at the right.
    -  Acceleration         Off, Low, Medium or High. With acceleration, moving the mouse quickly makes the pointer travel farther.
    +  Acceleration         Off, Low, Medium, or High. With acceleration, moving the mouse quickly makes the pointer travel farther.
       Cursor Speed         How far the pointer moves when you move the mouse. Toward Fast moves it farther.
     .endtable
     
    @@ -108,9 +108,9 @@ If the pointer is hard to control, set Acceleration to Off and adjust Cursor Spe
     
     .h2 Changing One Color
     
    -The System Colors list names every part of the screen that has a color, such as the desktop, title bars, menus and buttons. Click a name to see its color beside the list, with its red, green and blue amounts (each from 0, none of that color, to 255, full).
    +The System Colors list names every part of the screen that has a color, such as the desktop, title bars, menus, and buttons. Click a name to see its color beside the list, with its red, green, and blue amounts (each from 0, none of that color, to 255, full).
     
    -To change it, click Change... (or double-click the name). A color box named after the part opens: click one of the basic colors, or mix your own with the Red, Green and Blue sliders or by typing the numbers, and watch the preview. Click OK and the screen changes to the new color at once; Cancel leaves it as it was.
    +To change it, click Change... (or double-click the name). A color box named after the part opens: click one of the basic colors, or mix your own with the Red, Green, and Blue sliders or by typing the numbers, and watch the preview. Click OK and the screen changes to the new color at once; Cancel leaves it as it was.
     
     .h2 Themes
     
    @@ -141,7 +141,7 @@ The desktop can show a picture, called wallpaper, behind all the windows.
     
     .list
     .item The list shows the pictures that came with DVX. Click one and click Apply, or double-click it.
    -.item Browse... lets you pick any BMP, JPEG, PNG or GIF picture from any folder.
    +.item Browse... lets you pick any BMP, JPEG, PNG, or GIF picture from any folder.
     .item Clear removes the picture, leaving the plain desktop color.
     .item The line under the list shows the picture in use, or (none).
     .endlist
    @@ -165,6 +165,8 @@ If a picture cannot be loaded, the Control Panel says so and the desktop stays a
     .index Screen Resolution
     .index Resolution
     .index Color Depth
    +.index Smooth Text
    +.index Text smoothing
     
     .h1 Screen Resolution and Colors
     
    @@ -181,6 +183,10 @@ So if the screen becomes hard to read, or goes blank, just wait 15 seconds.
     
     Click OK on the Control Panel to use the new mode again the next time you start DVX.
     
    +.h2 Smooth Text
    +
    +Smooth the edges of text softens the jagged steps on the curves and slants of letters by drawing their edges in shades between the text color and the background, the way printed text looks. It is on unless you turn it off, and the change shows at once in every window. Smoothing needs a mode with more than 256 colors; in a 256-color mode text is drawn sharp whatever the box says. Only the fonts and sizes that come smoothed are drawn this way -- the system font is always sharp. Click OK to keep the setting, or Cancel to go back.
    +
     If DVX ever starts with a screen you cannot use, hold down a Shift key while DVX starts: it then starts in 640x480, the mode every video card can show, and offers to keep it. The DVX help (Troubleshooting) explains this "safe video mode".
     
     .note tip
    @@ -232,10 +238,10 @@ The network card is used through its packet driver: a small program for your car
     .table
       Setting                                Meaning
       -------                                -------
    -  Get the address automatically (DHCP)   The network gives DVX its address, mask, gateway and name servers. Most networks, and most emulators, work this way.
    -  Address, Subnet mask, Gateway          Only when not automatic: the address for this computer (such as 192.168.1.10), the mask (usually 255.255.255.0) and the router to other networks (may be empty).
    +  Get the address automatically (DHCP)   The network gives DVX its address, mask, gateway, and name servers. Most networks, and most emulators, work this way.
    +  Address, Subnet mask, Gateway          Only when not automatic: the address for this computer (such as 192.168.1.10), the mask (usually 255.255.255.0), and the router to other networks (may be empty).
       Name server 1, 2                       Computers that turn names such as bbs.example.org into addresses. Leave them empty to use the ones the network gives out; filled in, they are used instead.
    -  Computer name                          This computer's name on the network (letters, digits and hyphens).
    +  Computer name                          This computer's name on the network (letters, digits, and hyphens).
       Packet driver                          "Find it" looks for the packet driver by itself. Pick an interrupt only if it is not found.
     .endtable
     
    @@ -256,7 +262,7 @@ The Network Status window shows, updated every second:
     
     .list
     .item What the network is doing: "Connected", "Waiting for a DHCP server", or that no packet driver was found.
    -.item The address, subnet mask and gateway in use, and the name servers.
    +.item The address, subnet mask, and gateway in use, and the name servers.
     .item The network card (its packet driver's name and hardware address).
     .item How much has been received and sent. "Lost" counts arrivals DVX was too busy to take (for a connection, the other computer sends them again).
     .endlist
    @@ -276,12 +282,12 @@ The Printer tab tells DVX about your printer. Every program prints to the printe
     .table
       Setting         What it does
       -------         ------------
    -  Printer model   Text only works with any printer, but prints only text. Choose Epson 9-pin, Epson 24-pin or HP LaserJet if your printer is one of those or works like one (most dot matrix printers work like an Epson, most laser printers like a LaserJet); then pictures can be printed too.
    -  Connected to    The printer port the printer is plugged into: LPT1, LPT2 or LPT3. Choose File to print into a file instead.
    +  Printer model   Text only works with any printer, but prints only text. Choose Epson 9-pin, Epson 24-pin, or HP LaserJet if your printer is one of those or works like one (most dot matrix printers work like an Epson, most laser printers like a LaserJet); then pictures can be printed too.
    +  Connected to    The printer port the printer is plugged into: LPT1, LPT2, or LPT3. Choose File to print into a file instead.
       File name       The file printouts go into when Connected to is File. Each printout replaces it.
     .endtable
     
    -Page Setup... opens the Page Setup dialog: the margins, lines per page, header and footer used when text is printed. Its changes are saved as soon as you choose OK in that dialog.
    +Page Setup... opens the Page Setup dialog: the margins, lines per page, header, and footer used when text is printed. Its changes are saved as soon as you choose OK in that dialog.
     
     Like the other tabs, the model and port are saved when you click OK.
     
    @@ -298,7 +304,7 @@ Like the other tabs, the model and port are saved when you click OK.
     
     .h1 Game Controllers
     
    -The Game Controllers tab shows the joysticks DVX found, lets you try them out and calibrates them so games read them correctly. When no joystick is connected, the keyboard can stand in for one.
    +The Game Controllers tab shows the joysticks DVX found, lets you try them out, and calibrates them so games read them correctly. When no joystick is connected, the keyboard can stand in for one.
     
     .table
       Setting       What it does
    @@ -308,7 +314,7 @@ The Game Controllers tab shows the joysticks DVX found, lets you try them out an
       Dead zone     How far the stick can move from the middle and still count as the middle, in percent. A worn stick that does not come back to the middle exactly needs a bigger dead zone.
       Game port     Two joysticks: the game port has room for two joysticks with two buttons each. One joystick with 4 axes: for flight sticks with a throttle or rudder, which use the whole port and all four buttons.
       Keyboard      When to offer the keyboard as a joystick: when no joystick is found (the usual choice), always, or never.
    -  Keys...       Chooses the keys the keyboard joystick uses: up, down, left, right and four buttons. At first they are the arrow keys, Left Ctrl, Space, Left Shift and Z.
    +  Keys...       Chooses the keys the keyboard joystick uses: up, down, left, right, and four buttons. At first they are the arrow keys, Left Ctrl, Space, Left Shift, and Z.
     .endtable
     
     .h2 Trying Out a Joystick
    @@ -344,7 +350,7 @@ Every change works at once, so you can see it in the picture. Like the other tab
     
     .h1 Sounds
     
    -The Sounds tab chooses the sound DVX makes for each event, such as starting, ending or showing a message, sets the overall volume, and says which sound hardware to use. Without sound hardware, everything works the same but silently.
    +The Sounds tab chooses the sound DVX makes for each event, such as starting, ending, or showing a message, sets the overall volume, and says which sound hardware to use. Without sound hardware, everything works the same but silently.
     
     .table
       Setting        What it does
    diff --git a/src/apps/kpunch/dvxbasic/Makefile b/src/apps/kpunch/dvxbasic/Makefile
    index 531dbbfb..c8c25c1b 100644
    --- a/src/apps/kpunch/dvxbasic/Makefile
    +++ b/src/apps/kpunch/dvxbasic/Makefile
    @@ -68,7 +68,7 @@ VM_SRCS      = runtime/vm.c runtime/values.c runtime/dates.c runtime/serialize.c
     # at run time on the target (INI access through dvxPrefs).
     VM_LINK_SRCS = $(VM_SRCS) $(LIBDVX)/dvxPrefs.c
     SUPPORT_SRCS = $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/thirdparty/stb_ds_impl.c
    -RES_SRCS     = $(LIBDVX)/dvxResource.c $(TOOLSDIR)/dvxResWrite.c
    +RES_SRCS     = $(LIBDVX)/dvxResource.c $(LIBDVX)/dvxFontFmt.c $(TOOLSDIR)/dvxResWrite.c
     CLI_SRCS     = basBuild.c $(COMP_SRCS) $(VM_LINK_SRCS) $(RES_SRCS) $(SUPPORT_SRCS)
     
     # Runtime library objects (VM + values + form runtime + serialization)
    diff --git a/src/apps/kpunch/dvxbasic/basBuild.c b/src/apps/kpunch/dvxbasic/basBuild.c
    index c08cb54f..b4776db9 100644
    --- a/src/apps/kpunch/dvxbasic/basBuild.c
    +++ b/src/apps/kpunch/dvxbasic/basBuild.c
    @@ -53,6 +53,7 @@
     #include "compiler/obfuscate.h"
     #include "compiler/strip.h"
     #include "runtime/serialize.h"
    +#include "dvxFontFmt.h"
     #include "dvxRes.h"
     #include "dvxTypes.h"
     #include "dvxPlat.h"
    @@ -78,16 +79,15 @@
     // Room a wrapper line needs besides its directive, name and control
     // list: the space, the quotes, the newline and the NUL.
     #define BAS_BUILD_WRAP_SLACK   8
    -// Longest [Modules]/[Forms] key, "File" plus a decimal index.
    -#define BAS_BUILD_KEY_LEN      32
     // How a compiler message starts when it names the line ("Line N, Col M: ")
     #define BAS_BUILD_LINE_PREFIX  "Line "
     #define BAS_BUILD_ERR_MEMORY   "Out of memory."
     #define BAS_BUILD_ERR_MODULE   "Failed to build module."
     
    -// emitIcon's error text when it names the unreadable icon path (the
    -// other build errors are string literals)
    +// emitIcon's and emitFonts' error texts when they name a file (the other
    +// build errors are string literals)
     static char sIconError[BAS_BUILD_ERROR_LEN];
    +static char sFontError[BAS_BUILD_ERROR_LEN];
     
     // basBuildConcatProject passes, in source order.
     typedef enum {
    @@ -142,6 +142,7 @@ static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec);
     static int32_t     countLines(const char *text, int32_t len);
     static const char *emitControls(const char *outPath, const BasBuildSpecT *spec);
     static const char *emitCtlTexts(const char *outPath, const BasBuildSpecT *spec);
    +static const char *emitFonts(const char *outPath, const BasBuildSpecT *spec);
     static const char *emitIcon(const char *outPath, const BasBuildSpecT *spec);
     static const char *emitResources(const char *outPath, const BasBuildSpecT *spec, const uint8_t *modData, int32_t modLen, const uint8_t *dbgData, int32_t dbgLen, uint8_t **frmData, const int32_t *frmLens, const BasObfFrmT *obfFrms, int32_t frmCount);
     static void        freeFrmWork(uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms);
    @@ -356,6 +357,10 @@ const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec) {
             failure = emitResources(outPath, spec, modData, modLen, dbgData, dbgLen, frmData, frmLens, obfFrms, frmCount);
         }
     
    +    if (!failure) {
    +        failure = emitFonts(outPath, spec);
    +    }
    +
         if (!failure) {
             failure = copyHelpFile(outPath, spec);
         }
    @@ -481,8 +486,8 @@ bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProje
         // then .frm code sections.
         for (int32_t pass = 0; pass < BAS_BUILD_PASS_COUNT; pass++) {
             for (int32_t i = 0; ; i++) {
    -            char key[BAS_BUILD_KEY_LEN];
    -            snprintf(key, sizeof(key), "File%d", (int)i);
    +            char key[BAS_INI_KEY_FILE_LEN];
    +            snprintf(key, sizeof(key), BAS_INI_KEY_FILE_FMT, i);
                 const char *val = prefsGetString(prefs, sections[pass], key, NULL);
     
                 if (!val) {
    @@ -661,6 +666,27 @@ void basBuildSpecFromProject(PrefsHandleT *prefs, const char *projectDir, BasBui
         spec->helpFile      = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, "");
         spec->multiInstance = prefsGetBool(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_MULTIINSTANCE, false);
         spec->projectDir    = projectDir;
    +    spec->fontCount     = 0;
    +
    +    // [Fonts] File0, File1, ...: every one counted, the first
    +    // BAS_BUILD_FONTS_MAX kept
    +    for (;;) {
    +        char        key[BAS_INI_KEY_FILE_LEN];
    +        const char *val;
    +
    +        snprintf(key, sizeof(key), BAS_INI_KEY_FILE_FMT, spec->fontCount);
    +        val = prefsGetString(prefs, BAS_INI_SECTION_FONTS, key, NULL);
    +
    +        if (!val) {
    +            break;
    +        }
    +
    +        if (spec->fontCount < BAS_BUILD_FONTS_MAX) {
    +            spec->fonts[spec->fontCount] = val;
    +        }
    +
    +        spec->fontCount++;
    +    }
     }
     
     
    @@ -813,6 +839,59 @@ static const char *emitCtlTexts(const char *outPath, const BasBuildSpecT *spec)
     }
     
     
    +// The fonts the program carries as FONTn resources, each checked to be a
    +// DVX font first
    +static const char *emitFonts(const char *outPath, const BasBuildSpecT *spec) {
    +    if (spec->fontCount > BAS_BUILD_FONTS_MAX) {
    +        snprintf(sFontError, sizeof(sFontError), "A program carries at most %d fonts.", BAS_BUILD_FONTS_MAX);
    +        return sFontError;
    +    }
    +
    +    for (int32_t i = 0; i < spec->fontCount; i++) {
    +        char       path[DVX_MAX_PATH];
    +        char       name[BAS_RES_FONT_NAME_LEN];
    +        char       err[DFN_ERR_MAX];
    +        int32_t    len  = 0;
    +        uint8_t   *data = NULL;
    +        DfnSourceT src;
    +        DfnFileT   f;
    +
    +        platformPathJoin(path, sizeof(path), spec->projectDir, spec->fonts[i]);
    +        data = (uint8_t *)platformReadFile(path, &len);
    +
    +        if (!data) {
    +            snprintf(sFontError, sizeof(sFontError), "Cannot read font %s", path);
    +            return sFontError;
    +        }
    +
    +        memset(&src, 0, sizeof(src));
    +        src.name   = path;
    +        src.data   = data;
    +        src.length = (uint32_t)len;
    +
    +        if (!dfnOpen(&src, &f, err, sizeof(err))) {
    +            free(data);
    +            snprintf(sFontError, sizeof(sFontError), "%s", err);
    +            return sFontError;
    +        }
    +
    +        snprintf(name, sizeof(name), BAS_RES_FONT_FMT, (long)i);
    +
    +        int32_t rc = dvxResAppend(outPath, name, DVX_RES_FONT, data, (uint32_t)len);
    +
    +        free(data);
    +
    +        if (rc != 0) {
    +            return "Failed to write a font resource.";
    +        }
    +
    +        buildLog(spec, "Font %s", spec->fonts[i]);
    +    }
    +
    +    return NULL;
    +}
    +
    +
     // Embed the project icon, or the NOICON fallback of the stub carrier
     // when the project names none.  Returns NULL on success, else
     // an error message (a named icon that cannot be read names the path).
    diff --git a/src/apps/kpunch/dvxbasic/basBuild.h b/src/apps/kpunch/dvxbasic/basBuild.h
    index 6947db2f..b9c9c013 100644
    --- a/src/apps/kpunch/dvxbasic/basBuild.h
    +++ b/src/apps/kpunch/dvxbasic/basBuild.h
    @@ -48,6 +48,9 @@
     extern "C" {
     #endif
     
    +// The most fonts a program carries ([Fonts]; each a font resource)
    +#define BAS_BUILD_FONTS_MAX 64
    +
     // Progress callback (may be NULL); receives one line per pipeline stage.
     typedef void (*BasBuildLogFnT)(const char *msg);
     
    @@ -83,6 +86,12 @@ typedef struct {
         const char *const *ctlSources;
         int32_t            ctlCount;
     
    +    // .DFN fonts the program carries, relative to projectDir: FONTn
    +    // resources (programs only).  fontCount may pass BAS_BUILD_FONTS_MAX
    +    // (the project lists more): the build then refuses.
    +    const char *fonts[BAS_BUILD_FONTS_MAX];
    +    int32_t     fontCount;
    +
         bool           release;
         bool           multiInstance;  // [Settings] MultiInstance: more than one copy may run
         BasBuildLogFnT log;            // may be NULL
    diff --git a/src/apps/kpunch/dvxbasic/basRes.h b/src/apps/kpunch/dvxbasic/basRes.h
    index 3c2a5380..03b7bcac 100644
    --- a/src/apps/kpunch/dvxbasic/basRes.h
    +++ b/src/apps/kpunch/dvxbasic/basRes.h
    @@ -31,6 +31,7 @@
     #ifndef BAS_RES_H
     #define BAS_RES_H
     
    +#include  // PRId32
     #include 
     #include  // NULL
     #include 
    @@ -74,6 +75,12 @@ const char *dvxSkipWs(const char *s);
     #define BAS_INI_KEY_TYPE            "Type"            // [Project] Type: Program (the default) or Control
     #define BAS_PROJECT_TYPE_CONTROL    "Control"         // a control project: builds NAME.wgt
     #define BAS_INI_SECTION_FORMS       "Forms"
    +#define BAS_INI_SECTION_FONTS       "Fonts"           // .DFN fonts the program carries (font resources)
    +
    +// A list section's keys: File0, File1, ... (readers stop at the first
    +// one missing), and room for one with its NUL
    +#define BAS_INI_KEY_FILE_FMT        "File%" PRId32
    +#define BAS_INI_KEY_FILE_LEN        16
     
     // ------------------------------------------------------------
     // .cls class module files
    @@ -151,6 +158,11 @@ const char *dvxSkipWs(const char *s);
     // controls) and in a control file (.wgt).  BAS_MAX_CTL_RESOURCES bounds
     // the writer and the readers alike.
     #define BAS_RES_CTL_FMT      "CTL%ld"
    +
    +// A font the program carries: FONT0, FONT1, ... (DVX_RES_FONT; the shell
    +// adds them for the program when it loads it)
    +#define BAS_RES_FONT_FMT     "FONT%ld"
    +#define BAS_RES_FONT_NAME_LEN 16       // buffer for a formatted FONTn resource name
     #define BAS_MAX_CTL_RESOURCES 64
     
     // A widget file's toolbox resources (formrt basCtlResPath, the IDE
    diff --git a/src/apps/kpunch/dvxbasic/basdev/basDevBas.c b/src/apps/kpunch/dvxbasic/basdev/basDevBas.c
    index d6b33857..10b58f32 100644
    --- a/src/apps/kpunch/dvxbasic/basdev/basDevBas.c
    +++ b/src/apps/kpunch/dvxbasic/basdev/basDevBas.c
    @@ -263,6 +263,7 @@ static void       *handleOf(int32_t handle, DevKindE kind);
     static void        handleRelease(int32_t idx);
     static void        moduleRelease(DevModuleT *m);
     int32_t            ProjAddFile(int32_t prj, const char *fileName);
    +int32_t            ProjAddFont(int32_t prj, const char *fileName);
     void               ProjClose(int32_t prj);
     const char        *ProjCtlHeader(const char *text, const char *key);
     const char        *ProjCtlKeys(void);
    @@ -273,12 +274,15 @@ int32_t            ProjFileCount(int32_t prj);
     int32_t            ProjFileFind(int32_t prj, const char *fileName);
     int32_t            ProjFileKind(int32_t prj, int32_t idx);
     const char        *ProjFilePath(int32_t prj, int32_t idx);
    +const char        *ProjFont(int32_t prj, int32_t idx);
    +int32_t            ProjFontCount(int32_t prj);
     const char        *ProjGet(int32_t prj, const char *key);
     static int32_t     projHandle(PrjStateT *p);
     int32_t            ProjNew(const char *name, const char *folder);
     int32_t            ProjOpen(const char *dbpPath);
     const char        *ProjPath(int32_t prj);
     int32_t            ProjRemoveFile(int32_t prj, int32_t idx);
    +int32_t            ProjRemoveFont(int32_t prj, int32_t idx);
     int32_t            ProjRenameFile(int32_t prj, int32_t idx, const char *newName);
     int32_t            ProjSave(int32_t prj);
     int32_t            ProjSet(int32_t prj, const char *key, const char *value);
    @@ -369,7 +373,7 @@ int32_t CompBuildApp(int32_t mdl, int32_t src, const char *outPath, int32_t rele
         BasBuildSpecT spec;
     
         if (!m || !s || !outPath || !outPath[0]) {
    -        compFail("No compiled program, source set or output file.");
    +        compFail("No compiled program, source set, or output file.");
             return BAS_FALSE;
         }
     
    @@ -1321,6 +1325,26 @@ int32_t ProjAddFile(int32_t prj, const char *fileName) {
     }
     
     
    +// Add a .DFN font the built program carries (path relative to the
    +// project's folder).  Its index, or -1 when it is listed already.
    +int32_t ProjAddFont(int32_t prj, const char *fileName) {
    +    PrjStateT *p = (PrjStateT *)handleOf(prj, DevProjectE);
    +
    +    if (!p || !fileName || !fileName[0]) {
    +        compFail("No project or font file name.");
    +        return DEV_NO_INDEX;
    +    }
    +
    +    int32_t idx = prjAddFont(p, fileName);
    +
    +    if (idx < 0) {
    +        compFail("The font is already in the project.");
    +    }
    +
    +    return idx;
    +}
    +
    +
     void ProjClose(int32_t prj) {
         if (handleOf(prj, DevProjectE)) {
             handleRelease(prj - 1);
    @@ -1443,6 +1467,23 @@ const char *ProjFilePath(int32_t prj, int32_t idx) {
     }
     
     
    +// Font idx the program carries, as the project lists it (relative to its
    +// folder; "" out of range).
    +const char *ProjFont(int32_t prj, int32_t idx) {
    +    PrjStateT  *p    = (PrjStateT *)handleOf(prj, DevProjectE);
    +    const char *path = p ? prjFontPath(p, idx) : NULL;
    +
    +    return path ? path : "";
    +}
    +
    +
    +int32_t ProjFontCount(int32_t prj) {
    +    PrjStateT *p = (PrjStateT *)handleOf(prj, DevProjectE);
    +
    +    return p ? prjFontCount(p) : 0;
    +}
    +
    +
     // A [Project] / [Settings] value by its .dbp key ("" for an unknown key).
     const char *ProjGet(int32_t prj, const char *key) {
         PrjStateT *p = (PrjStateT *)handleOf(prj, DevProjectE);
    @@ -1532,6 +1573,19 @@ int32_t ProjRemoveFile(int32_t prj, int32_t idx) {
     }
     
     
    +// Remove font idx from the ones the program carries (the file stays).
    +int32_t ProjRemoveFont(int32_t prj, int32_t idx) {
    +    PrjStateT *p = (PrjStateT *)handleOf(prj, DevProjectE);
    +
    +    if (!p || idx < 0 || idx >= prjFontCount(p)) {
    +        return BAS_FALSE;
    +    }
    +
    +    prjRemoveFont(p, idx);
    +    return BAS_TRUE;
    +}
    +
    +
     // Rename file idx on disk and in the project (the .dbp is saved at
     // once).  False with CompError$ saying why.
     int32_t ProjRenameFile(int32_t prj, int32_t idx, const char *newName) {
    diff --git a/src/apps/kpunch/dvxbasic/basdev/basProject.c b/src/apps/kpunch/dvxbasic/basdev/basProject.c
    index 9d65ef79..ba4d1c99 100644
    --- a/src/apps/kpunch/dvxbasic/basdev/basProject.c
    +++ b/src/apps/kpunch/dvxbasic/basdev/basProject.c
    @@ -41,6 +41,9 @@
     //   [Forms]
     //   File0 = FORM1.FRM
     //
    +//   [Fonts]               (.DFN fonts the built program carries)
    +//   File0 = TITLE.DFN
    +//
     //   [Settings]
     //   StartupForm = Form1
     //
    @@ -63,11 +66,10 @@
     #include 
     #include 
     
    -#define PRJ_INI_KEY_LEN   16        // "FileNNN" INI key buffer size
    -#define PRJ_FILE_KEY_FMT  "File%" PRId32  // INI key for an indexed module/class/form path
     
     _Static_assert(DVX_MAX_PATH <= PRJ_MAX_FIELD && PRJ_MAX_STRING <= PRJ_MAX_FIELD, "every [Project] field must fit PRJ_MAX_FIELD");
     
    +#define PRJ_NUMBER_LEN    16        // a number as text with its NUL
     #define PRJ_TRUE_TEXT     "true"    // a flag setting as prjFieldGet gives it (and the .dbp stores it)
     #define PRJ_FALSE_TEXT    "false"
     #define PRJ_YES_TEXT      "yes"     // also a true flag (as the .dbp reader takes it)
    @@ -179,6 +181,7 @@ typedef struct {
     // ============================================================
     
     int32_t                 prjAddFile(PrjStateT *prj, const char *relativePath, PrjFileKindE kind);
    +int32_t                 prjAddFont(PrjStateT *prj, const char *relativePath);
     int32_t                 prjClassCodePos(const char *text, int32_t len, int32_t *outFirstLine);
     bool                    prjClassName(const PrjStateT *prj, int32_t fileIdx, const char *text, char *out, int32_t outSize);
     void                    prjClose(PrjStateT *prj);
    @@ -195,6 +198,8 @@ bool                    prjFieldGet(const PrjStateT *prj, const char *key, char
     bool                    prjFieldSet(PrjStateT *prj, const char *key, const char *value);
     int32_t                 prjFileCount(const PrjStateT *prj);
     int32_t                 prjFileIndexByPath(const PrjStateT *prj, const char *path);
    +int32_t                 prjFontCount(const PrjStateT *prj);
    +const char             *prjFontPath(const PrjStateT *prj, int32_t idx);
     void                    prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize);
     void                    prjInit(PrjStateT *prj);
     bool                    prjIsControlProject(const PrjStateT *prj);
    @@ -212,6 +217,7 @@ bool                    prjPathExists(const char *path);
     static void             prjPortableSeps(char *path);
     char                   *prjReadFileText(const PrjStateT *prj, int32_t fileIdx, bool *outMissing);
     void                    prjRemoveFile(PrjStateT *prj, int32_t idx);
    +void                    prjRemoveFont(PrjStateT *prj, int32_t idx);
     bool                    prjRenameFile(PrjStateT *prj, int32_t idx, const char *newName, char *err, int32_t errSize);
     bool                    prjSave(const PrjStateT *prj);
     static void             prjSaveFields(PrefsHandleT *h, const PrjStateT *prj, const char *section);
    @@ -231,6 +237,25 @@ int32_t prjAddFile(PrjStateT *prj, const char *relativePath, PrjFileKindE kind)
     }
     
     
    +int32_t prjAddFont(PrjStateT *prj, const char *relativePath) {
    +    PrjFontT font;
    +
    +    memset(&font, 0, sizeof(font));
    +    snprintf(font.path, sizeof(font.path), "%s", relativePath);
    +    platformPathNative(font.path);
    +
    +    for (int32_t i = 0; i < prjFontCount(prj); i++) {
    +        if (platformPathSame(prj->fonts[i].path, font.path)) {
    +            return -1;
    +        }
    +    }
    +
    +    arrput(prj->fonts, font);
    +    prj->dirty = true;
    +    return prjFontCount(prj) - 1;
    +}
    +
    +
     int32_t prjClassCodePos(const char *text, int32_t len, int32_t *outFirstLine) {
         int32_t codePos = basClassCodePos(text, len);
         int32_t line    = 1;
    @@ -265,6 +290,7 @@ void prjClose(PrjStateT *prj) {
     
         arrfree(prj->files);
         arrfree(prj->sourceMap);
    +    arrfree(prj->fonts);
         prjInit(prj);
     }
     
    @@ -514,7 +540,7 @@ static bool prjCtlLocate(const char *text, const PrjCtlKeyT *k, PrjCtlSpotT *spo
     
     char *prjCtlNew(const char *name) {
         char    stub[BAS_MAX_IDENT * 2];
    -    char    size[PRJ_INI_KEY_LEN];
    +    char    size[PRJ_NUMBER_LEN];
         int32_t stubLen  = snprintf(stub, sizeof(stub), PRJ_HANDLER_FMT, PRJ_PRIVATE, PRJ_CTL_PAINT, "");
         size_t  headSize = sizeof(PRJ_CTL_NEW_HEAD) + BAS_MAX_IDENT + (size_t)stubLen;
         char   *head     = (name && strlen(name) < BAS_MAX_IDENT && basIsValidIdent(name)) ? (char *)malloc(headSize) : NULL;
    @@ -613,6 +639,16 @@ int32_t prjFileIndexByPath(const PrjStateT *prj, const char *path) {
     }
     
     
    +int32_t prjFontCount(const PrjStateT *prj) {
    +    return (int32_t)arrlen(prj->fonts);
    +}
    +
    +
    +const char *prjFontPath(const PrjStateT *prj, int32_t idx) {
    +    return (idx >= 0 && idx < prjFontCount(prj)) ? prj->fonts[idx].path : NULL;
    +}
    +
    +
     void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize) {
         if (fileIdx < 0 || fileIdx >= prjFileCount(prj)) {
             outPath[0] = '\0';
    @@ -682,6 +718,21 @@ bool prjLoad(PrjStateT *prj, const char *dbpPath) {
             prjLoadFileSection(prj, h, (PrjFileKindE)kind);
         }
     
    +    // [Fonts]: the fonts the program carries
    +    for (int32_t i = 0; ; i++) {
    +        char        key[BAS_INI_KEY_FILE_LEN];
    +        const char *val;
    +
    +        snprintf(key, sizeof(key), BAS_INI_KEY_FILE_FMT, i);
    +        val = prefsGetString(h, BAS_INI_SECTION_FONTS, key, NULL);
    +
    +        if (!val) {
    +            break;
    +        }
    +
    +        prjAddFont(prj, val);
    +    }
    +
         prefsClose(h);
         prj->dirty = false;
         return true;
    @@ -744,8 +795,8 @@ static void prjLoadFields(PrjStateT *prj, PrefsHandleT *h) {
     // fixed cap).
     static void prjLoadFileSection(PrjStateT *prj, PrefsHandleT *h, PrjFileKindE kind) {
         for (int32_t i = 0; ; i++) {
    -        char key[PRJ_INI_KEY_LEN];
    -        snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, i);
    +        char key[BAS_INI_KEY_FILE_LEN];
    +        snprintf(key, sizeof(key), BAS_INI_KEY_FILE_FMT, i);
     
             const char *val = prefsGetString(h, sKinds[kind].section, key, NULL);
     
    @@ -911,6 +962,14 @@ void prjRemoveFile(PrjStateT *prj, int32_t idx) {
     }
     
     
    +void prjRemoveFont(PrjStateT *prj, int32_t idx) {
    +    if (idx >= 0 && idx < prjFontCount(prj)) {
    +        arrdel(prj->fonts, idx);
    +        prj->dirty = true;
    +    }
    +}
    +
    +
     bool prjRenameFile(PrjStateT *prj, int32_t idx, const char *newName, char *err, int32_t errSize) {
         PrjFileT   *file = &prj->files[idx];
         const char *ext  = sKinds[file->kind].ext;
    @@ -1012,6 +1071,16 @@ bool prjSave(const PrjStateT *prj) {
             prjSaveFileSection(h, prj, (PrjFileKindE)kind);
         }
     
    +    for (int32_t i = 0; i < prjFontCount(prj); i++) {
    +        char key[BAS_INI_KEY_FILE_LEN];
    +        char path[DVX_MAX_PATH];
    +
    +        snprintf(key, sizeof(key), BAS_INI_KEY_FILE_FMT, i);
    +        snprintf(path, sizeof(path), "%s", prj->fonts[i].path);
    +        prjPortableSeps(path);
    +        prefsSetString(h, BAS_INI_SECTION_FONTS, key, path);
    +    }
    +
         prjSaveFields(h, prj, BAS_INI_SECTION_SETTINGS);
     
         bool ok = prefsSaveAs(h, prj->projectPath);
    @@ -1057,10 +1126,10 @@ static void prjSaveFileSection(PrefsHandleT *h, const PrjStateT *prj, PrjFileKin
     
         for (int32_t i = 0; i < prjFileCount(prj); i++) {
             if (prj->files[i].kind == kind) {
    -            char key[PRJ_INI_KEY_LEN];
    +            char key[BAS_INI_KEY_FILE_LEN];
                 char path[DVX_MAX_PATH];
     
    -            snprintf(key, sizeof(key), PRJ_FILE_KEY_FMT, idx++);
    +            snprintf(key, sizeof(key), BAS_INI_KEY_FILE_FMT, idx++);
                 snprintf(path, sizeof(path), "%s", prj->files[i].path);
                 prjPortableSeps(path);
                 prefsSetString(h, sKinds[kind].section, key, path);
    diff --git a/src/apps/kpunch/dvxbasic/basdev/basProject.h b/src/apps/kpunch/dvxbasic/basdev/basProject.h
    index 6a4a0fca..84ef9963 100644
    --- a/src/apps/kpunch/dvxbasic/basdev/basProject.h
    +++ b/src/apps/kpunch/dvxbasic/basdev/basProject.h
    @@ -96,6 +96,11 @@ typedef struct {
         bool     modified;           // true = buffer has unsaved changes
     } PrjFileT;
     
    +// A font the program carries ([Fonts]; built in as a font resource)
    +typedef struct {
    +    char path[DVX_MAX_PATH];     // relative to the project's folder
    +} PrjFontT;
    +
     // ============================================================
     // Project state
     // ============================================================
    @@ -104,6 +109,7 @@ typedef struct {
         // Fields are ordered by alignment to minimize struct padding.
         PrjFileT       *files;                      // stb_ds dynamic array (prjFileCount)
         BasSourceMapT  *sourceMap;                  // stb_ds array: path = files[].path, firstFileLine = first code line
    +    PrjFontT       *fonts;                      // stb_ds array: the fonts the program carries
         int32_t         activeFileIdx; // index of file open in editor (-1 = none)
         char            name[PRJ_MAX_NAME];
         char            type[PRJ_MAX_NAME];        // [Project] Type: "" a program, BAS_PROJECT_TYPE_CONTROL a control project (builds NAME.wgt)
    @@ -132,6 +138,10 @@ typedef struct {
     // index.
     int32_t prjAddFile(PrjStateT *prj, const char *relativePath, PrjFileKindE kind);
     
    +// Add a font the program carries (a .DFN; path relative to the project's
    +// directory): its index, -1 when it is listed already.
    +int32_t prjAddFont(PrjStateT *prj, const char *relativePath);
    +
     // Offset of the first line of code in the text of a class module (len
     // bytes), just past its header; *outFirstLine receives that line's
     // 1-based number.
    @@ -157,6 +167,11 @@ bool    prjFieldGet(const PrjStateT *prj, const char *key, char *out, int32_t ou
     bool    prjFieldSet(PrjStateT *prj, const char *key, const char *value);
     int32_t prjFileCount(const PrjStateT *prj);
     
    +// How many fonts the program carries, and font idx's path (relative to
    +// the project's directory; NULL out of range).
    +int32_t     prjFontCount(const PrjStateT *prj);
    +const char *prjFontPath(const PrjStateT *prj, int32_t idx);
    +
     // Index of the project file whose relative path is path (any case), or -1.
     int32_t prjFileIndexByPath(const PrjStateT *prj, const char *path);
     
    @@ -211,6 +226,9 @@ char   *prjReadFileText(const PrjStateT *prj, int32_t fileIdx, bool *outMissing)
     // Remove file idx from the project (the file on disk stays).
     void    prjRemoveFile(PrjStateT *prj, int32_t idx);
     
    +// Remove font idx from the fonts the program carries (the file stays).
    +void    prjRemoveFont(PrjStateT *prj, int32_t idx);
    +
     // Rename project file idx to newName, a file name in the project's
     // directory whose extension, when given, is the one of the file's kind
     // (none: it is added).  The file on disk (when it has been saved) is
    diff --git a/src/apps/kpunch/dvxbasic/basdev/basdev.dhs b/src/apps/kpunch/dvxbasic/basdev/basdev.dhs
    index b3f7fc2e..9757c7e9 100644
    --- a/src/apps/kpunch/dvxbasic/basdev/basdev.dhs
    +++ b/src/apps/kpunch/dvxbasic/basdev/basdev.dhs
    @@ -46,7 +46,7 @@
     
     .h1 BASIC Development Library
     
    -basdev.lib is the DVX BASIC compiler and what a development tool builds on it: the build pipeline (basBuild.h), the compile-time Control.Member validator (basValidate.h), the debugger's view of a paused program (basDebug.h) and the SUB / FUNCTION scanner (basProcs.h). The DVX BASIC IDE uses it, and BASIC programs reach it through compiler.bas and debug.bas (basDevBas.c holds their entry points). It also carries the STUB and noicon resources every build inside DVX uses (BAS_BUILD_STUB_CARRIER). It needs basrt.lib, which holds the run host (basRunHost.h) the IDE, the standalone stub and debug.bas share.
    +basdev.lib is the DVX BASIC compiler and what a development tool builds on it: the build pipeline (basBuild.h), the compile-time Control.Member validator (basValidate.h), the debugger's view of a paused program (basDebug.h), and the SUB / FUNCTION scanner (basProcs.h). The DVX BASIC IDE uses it, and BASIC programs reach it through compiler.bas and debug.bas (basDevBas.c holds their entry points). It also carries the STUB and noicon resources every build inside DVX uses (BAS_BUILD_STUB_CARRIER). It needs basrt.lib, which holds the run host (basRunHost.h) the IDE, the standalone stub, and debug.bas share.
     
     Headers: apps/dvxbasic/basBuild.h, apps/dvxbasic/basdev/basValidate.h, apps/dvxbasic/basdev/basDebug.h, apps/dvxbasic/basdev/basProcs.h, apps/dvxbasic/basdev/basSource.h, apps/dvxbasic/basdev/basHelpMap.h, apps/dvxbasic/basdev/basProject.h, apps/dvxbasic/formrt/basRunHost.h
     
    @@ -57,20 +57,20 @@ Headers: apps/dvxbasic/basBuild.h, apps/dvxbasic/basdev/basValidate.h, apps/dvxb
       --------                -----------
       basBuildAddSource       Append one file to a BasProjectSourceT the way a project is built: a module in BEGINMODULE, a class module's code in BEGINCLASS, a UserControl's code in BEGINCLASS ... USERCONTROL (its header is kept in ctlTexts), a form's code section in BEGINFORM with its controls (the .frm text is kept in frmTexts), a lone program unwrapped. Records the file in the source map.
       basBuildConcatProject   Read every file an open .dbp names (modules, class modules, UserControls, forms) into a BasProjectSourceT through basBuildAddSource.
    -  basBuildCompile         Compile source into a BasModuleT with OPTION EXPLICIT and an optional BasCtrlValidatorT. A BasCompileErrorT says why it failed: BAS_COMPILE_ERROR (line, column, message), BAS_COMPILE_NO_MEMORY or BAS_COMPILE_NO_MODULE.
    +  basBuildCompile         Compile source into a BasModuleT with OPTION EXPLICIT and an optional BasCtrlValidatorT. A BasCompileErrorT says why it failed: BAS_COMPILE_ERROR (line, column, message), BAS_COMPILE_NO_MEMORY, or BAS_COMPILE_NO_MODULE.
       basBuildErrorText       A compiler message without its "Line N, Col M: " prefix.
       basBuildMapLine         Line of the concatenated source to file and file line.
       basBuildMapFileLine     File and file line to the line of the concatenated source (names compared as DOS does: any case, / and \ alike).
       basBuildSpecFromProject Fill a BasBuildSpecT's project metadata from an open .dbp.
    -  basBuildApp             Build an .app from a module, the .frm texts and the .ctl headers (ctlSources). A NULL selfPath takes the stub from BAS_BUILD_STUB_CARRIER (this library).
    -  basBuildControl         Build a control file (.wgt, resources only) from a control project's module and .ctl headers: the module, each header as CTLn with its toolbox icon24 and name resources, the project's metadata and help file. The IDE's Make Control and CompBuildApp of a control project.
    +  basBuildApp             Build an .app from a module, the .frm texts, and the .ctl headers (ctlSources). A NULL selfPath takes the stub from BAS_BUILD_STUB_CARRIER (this library).
    +  basBuildControl         Build a control file (.wgt, resources only) from a control project's module and .ctl headers: the module, each header as CTLn with its toolbox icon24 and name resources, the project's metadata, and help file. The IDE's Make Control and CompBuildApp of a control project.
       basBuildIsControlProject  True when an open .dbp is a control project ([Project] Type = Control).
       basBuildWrapForPath     The kind of a project file by its extension.
     .endtable
     
     .h2 Validating Control Members (basValidate.h)
     
    -A BasValidatorT (zeroed) collects the forms, controls and menu items of the .frm texts basValidateScanFrm parses; its onCode hook also receives each form's code section, so one parse serves both. basValidateCallbacks fills the BasCtrlValidatorT the compiler takes; pass &validator.compiler as basBuildCompile's liveCompiler so a control name resolves on the form whose code is compiled. Member names are checked against the loaded widget interfaces (WgtIfaceT) and the form runtime's own property tables. badTypes lists controls whose type no loaded widget library registers (BAS_VALIDATE_BAD_TYPE_FMT formats one). The command-line compiler does not validate: no widget library is loaded there.
    +A BasValidatorT (zeroed) collects the forms, controls, and menu items of the .frm texts basValidateScanFrm parses; its onCode hook also receives each form's code section, so one parse serves both. basValidateCallbacks fills the BasCtrlValidatorT the compiler takes; pass &validator.compiler as basBuildCompile's liveCompiler so a control name resolves on the form whose code is compiled. Member names are checked against the loaded widget interfaces (WgtIfaceT) and the form runtime's own property tables. badTypes lists controls whose type no loaded widget library registers (BAS_VALIDATE_BAD_TYPE_FMT formats one). The command-line compiler does not validate: no widget library is loaded there.
     
     .h2 Debugging (basDebug.h)
     
    @@ -81,10 +81,10 @@ Every function takes the paused program's VM (or NULL when there is none).
       --------                -----------
       basDbgCurProcIdx        The compiled procedure the paused VM is in (-1: module level).
       basDbgProcNameForPc     Procedure name holding a code address (BAS_DBG_MODULE_FRAME before every procedure).
    -  basDbgVarVisible        Whether a debug variable is in scope where the VM is paused (QBasic rules: SHARED, STATIC, form and member variables).
    +  basDbgVarVisible        Whether a debug variable is in scope where the VM is paused (QBasic rules: SHARED, STATIC, form, and member variables).
       basDbgFindVar           The visible debug variable of a name.
       basDbgVarSlot           The live value slot of a debug variable.
    -  basDbgResolvePath       The live slot of var, var(i, j), var.field or var(i).field (a TYPE field or a class member).
    +  basDbgResolvePath       The live slot of var, var(i, j), var.field, or var(i).field (a TYPE field or a class member).
       basDbgFormatValue       Display text of a value (strings quoted, #dates#, array bounds, class names, Collection item counts).
       basDbgLocals            The visible variables as rows (name, type, value) through a BasDbgRowFnT.
       basDbgCallStack         The call stack as rows (procedure, line).
    @@ -98,11 +98,11 @@ Every function takes the paused program's VM (or NULL when there is none).
     
     .h2 Procedures (basProcs.h)
     
    -basProcDeclAt, basProcSkipToEnd and basProcNameFromDecl find the SUB / FUNCTION blocks of source text without compiling it (BAS_PROC_KW_END_SUB / BAS_PROC_KW_END_FUNCTION end them). The IDE's one-procedure-at-a-time editor and CompProcList$ use them.
    +basProcDeclAt, basProcSkipToEnd, and basProcNameFromDecl find the SUB / FUNCTION blocks of source text without compiling it (BAS_PROC_KW_END_SUB / BAS_PROC_KW_END_FUNCTION end them). The IDE's one-procedure-at-a-time editor and CompProcList$ use them.
     
     .h2 Source Text (basSource.h)
     
    -Source text read with the compiler's lexer, line by line, on text that need not compile: basSourceTokens reports every token with its kind (BasSrcKindE: keyword, identifier, string, number, date, symbol, comment, error), line, column and source span; basSourceInCode tells whether an offset is code (not in a string literal or a comment); basSourceRename renames a form or control (oldName followed by '.' or '_' as a whole word), optionally only in code; basSourceWithEvents and basSourceClassEvents report the WithEvents variables and the Events of a class module. The IDE's rename (ideRenameInCode, also after the Menu Editor renames items) and its Object / Event lists use them; compiler.bas has CompTokens$, CompInCode, CompRenameInCode$, CompWithEvents$ and CompClassEvents$.
    +Source text read with the compiler's lexer, line by line, on text that need not compile: basSourceTokens reports every token with its kind (BasSrcKindE: keyword, identifier, string, number, date, symbol, comment, error), line, column, and source span; basSourceInCode tells whether an offset is code (not in a string literal or a comment); basSourceRename renames a form or control (oldName followed by '.' or '_' as a whole word), optionally only in code; basSourceWithEvents and basSourceClassEvents report the WithEvents variables and the Events of a class module. The IDE's rename (ideRenameInCode, also after the Menu Editor renames items) and its Object / Event lists use them; compiler.bas has CompTokens$, CompInCode, CompRenameInCode$, CompWithEvents$, and CompClassEvents$.
     
     .h2 Help Topics (basHelpMap.h)
     
    @@ -110,18 +110,18 @@ basHelpKeywordTopic gives the DVX BASIC help topic of a keyword or built-in func
     
     .h2 Projects (basProject.h)
     
    -The project (.dbp) model: PrjStateT with its file list (PrjFileT: path relative to the project's folder, kind -- PrjFileKindE: module, class module, UserControl ([Controls], .ctl) or form, in compile order -- edit buffer) and settings, prjLoad / prjSave (one table of [Project] / [Settings] keys drives load, save, prjFieldGet and prjFieldSet; [Project] Type = Control makes a control project, prjIsControlProject), prjNew, prjAddFile, prjRemoveFile, prjRenameFile (disk and project together), prjFileIndexByPath, prjKindForPath (the build's rule, basBuildWrapForPath), prjKindWrap, prjClassName (a class module's or UserControl's class name), prjReadFileText and the source map of the last compile (prjMapLine, prjMapFileLine). The IDE keeps only its Project window and Properties dialog (ideProject.c); compiler.bas has ProjOpen, ProjNew, ProjSave, ProjClose, ProjPath$, ProjFileCount, ProjFile$, ProjFilePath$, ProjFileKind, ProjFileFind, ProjAddFile, ProjRemoveFile, ProjRenameFile, ProjGet$ and ProjSet; CompKeywordClass and CompKeywordReserved expose the keyword table's flags.
    +The project (.dbp) model: PrjStateT with its file list (PrjFileT: path relative to the project's folder, kind -- PrjFileKindE: module, class module, UserControl ([Controls], .ctl), or form, in compile order -- edit buffer) and settings, prjLoad / prjSave (one table of [Project] / [Settings] keys drives load, save, prjFieldGet, and prjFieldSet; [Project] Type = Control makes a control project, prjIsControlProject), prjNew, prjAddFile, prjRemoveFile, prjRenameFile (disk and project together), prjFileIndexByPath, prjKindForPath (the build's rule, basBuildWrapForPath), prjKindWrap, prjClassName (a class module's or UserControl's class name), prjReadFileText, the fonts the program carries ([Fonts]: prjAddFont, prjRemoveFont, prjFontCount, prjFontPath; the build makes them FONTn font resources, basBuildSpecT fonts), and the source map of the last compile (prjMapLine, prjMapFileLine). The IDE keeps only its Project window and Properties dialog (ideProject.c); compiler.bas has ProjOpen, ProjNew, ProjSave, ProjClose, ProjPath$, ProjFileCount, ProjFile$, ProjFilePath$, ProjFileKind, ProjFileFind, ProjAddFile, ProjRemoveFile, ProjRenameFile, ProjFontCount, ProjFont$, ProjAddFont, ProjRemoveFont, ProjGet$, and ProjSet; CompKeywordClass and CompKeywordReserved expose the keyword table's flags.
     
    -A UserControl's header (the Begin UserControl block of a .ctl) is read and changed key by key, every other line of the text kept: prjCtlKeyAt / prjCtlKeyFind give the keys (PrjCtlKeyT: name and PrjCtlValueE kind -- Name, the control's name on the Begin line, then Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, DefaultEvent), prjCtlHeaderGet the value the control takes (its default when the header leaves the key out), prjCtlHeaderSet a malloc'd copy of the text with one key changed, added before End or removed (an empty value), written as its kind is (a text in quotes, a color &HBBGGRR&, True or False), and prjCtlNew the text of a new UserControl (its header with Width and Height, Option Explicit, an empty UserControl_Paint written with PRJ_HANDLER_FMT and PRJ_PRIVATE, the form the IDE writes event handlers in). The IDE's File > New UserControl and Properties window use them; compiler.bas has ProjCtlNew$, ProjCtlHeader$, ProjCtlSetHeader$ and ProjCtlKeys$ (the keys and their kinds, prjCtlKeyAt order).
    +A UserControl's header (the Begin UserControl block of a .ctl) is read and changed key by key, every other line of the text kept: prjCtlKeyAt / prjCtlKeyFind give the keys (PrjCtlKeyT: name and PrjCtlValueE kind -- Name, the control's name on the Begin line, then Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, DefaultEvent), prjCtlHeaderGet the value the control takes (its default when the header leaves the key out), prjCtlHeaderSet a malloc'd copy of the text with one key changed, added before End, or removed (an empty value), written as its kind is (a text in quotes, a color &HBBGGRR&, True or False), and prjCtlNew the text of a new UserControl (its header with Width and Height, Option Explicit, an empty UserControl_Paint written with PRJ_HANDLER_FMT and PRJ_PRIVATE, the form the IDE writes event handlers in). The IDE's File > New UserControl and Properties window use them; compiler.bas has ProjCtlNew$, ProjCtlHeader$, ProjCtlSetHeader$, and ProjCtlKeys$ (the keys and their kinds, prjCtlKeyAt order).
     
     .h2 Running a Program (basRunHost.h, in basrt.lib)
     
    -basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path and its CONFIG / DATA folders, COMMAND$, the print, input and DoEvents callbacks, the screen window's name, the help file and whether runtime errors show their own box. basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs.
    +basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path and its CONFIG / DATA folders, COMMAND$, the print, input, and DoEvents callbacks, the screen window's name, the help file, and whether runtime errors show their own box. basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused, and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs.
     
     VM accessors a debugger host uses (vm.h): basVmSetAppPaths, basVmSetBreakpointCallback, basVmSetDebugPaused, basVmResume, basVmStop, basVmClearStepState, basVmGetCurrentLine, basVmGetErrorLine.
     
     .h2 From BASIC
     
    -compiler.bas and debug.bas declare the entry points (see Include Libraries in the BASIC language reference). Source sets, modules and runs are handles freed with their program. A run is polled: RunPoll runs the child for a bounded time and never waits. A pause in the child's module code returns RUN_PAUSED; a pause inside one of its event handlers (or its forms' Load code, inside RunStart) waits in place while the child pumps DVX events, so the caller's Timer and menus can step, continue or stop it; RunPoll called from there returns at once. RunFree while the child's code is on the stack stops it at once and releases it at the next library call after that code has unwound.
    +compiler.bas and debug.bas declare the entry points (see Include Libraries in the BASIC language reference). Source sets, modules, and runs are handles freed with their program. A run is polled: RunPoll runs the child for a bounded time and never waits. A pause in the child's module code returns RUN_PAUSED; a pause inside one of its event handlers (or its forms' Load code, inside RunStart) waits in place while the child pumps DVX events, so the caller's Timer and menus can step, continue, or stop it; RunPoll called from there returns at once. RunFree while the child's code is on the stack stops it at once and releases it at the next library call after that code has unwound.
     
    -CompDesignControls registers a source set's UserControls for the calling program's design forms (basCtlLoadDesign with its runtime, the current one: a child run's own when the child calls it) and holds the module -- one of the caller's own handles; another program's is refused -- until CompDesignRelease, the next CompDesignControls or the end of that runtime (appEnded for a program, runDestroyHost for a child run), CompModuleFree meanwhile only marking it.
    +CompDesignControls registers a source set's UserControls for the calling program's design forms (basCtlLoadDesign with its runtime, the current one: a child run's own when the child calls it) and holds the module -- one of the caller's own handles; another program's is refused -- until CompDesignRelease, the next CompDesignControls, or the end of that runtime (appEnded for a program, runDestroyHost for a child run), CompModuleFree meanwhile only marking it.
    diff --git a/src/apps/kpunch/dvxbasic/basrt.dhs b/src/apps/kpunch/dvxbasic/basrt.dhs
    index 01b12da1..f8b4e46e 100644
    --- a/src/apps/kpunch/dvxbasic/basrt.dhs
    +++ b/src/apps/kpunch/dvxbasic/basrt.dhs
    @@ -73,10 +73,10 @@ Header: apps/dvxbasic/runtime/values.h
       BAS_TYPE_ARRAY   6       arrVal          Reference-counted array.
       BAS_TYPE_UDT     7       udtVal          Reference-counted user-defined type.
       BAS_TYPE_OBJECT  8       objVal          Opaque host pointer (form, control).
    -  BAS_TYPE_REF     9       ref             Reference (ByRef argument): ref.target is the variable, element or field it names; ref.owner keeps that storage alive.
    +  BAS_TYPE_REF     9       ref             Reference (ByRef argument): ref.target is the variable, element, or field it names; ref.owner keeps that storage alive.
     .endtable
     
    -A reference only ever lives in a call's parameters (a ByRef parameter, a ParamArray element). Read through it with basValDeref, which returns the value a reference names (or the value itself). refKind says what ref.owner is: BAS_REF_SLOT (none: a global, local or form variable), BAS_REF_ARRAY (the array, counted and locked so REDIM / ERASE raise error 10), BAS_REF_UDT (the TYPE instance holding the field), BAS_REF_OBJECT (the class instance holding the member variable) or BAS_REF_CELL (a BasRefCellT holding both the locked array and the TYPE instance, for a field of an array element). refType is the target's declared type (stores convert to it) and refLen the length of a STRING * n target. basValCopy and basValRelease count the owner.
    +A reference only ever lives in a call's parameters (a ByRef parameter, a ParamArray element). Read through it with basValDeref, which returns the value a reference names (or the value itself). refKind says what ref.owner is: BAS_REF_SLOT (none: a global, local, or form variable), BAS_REF_ARRAY (the array, counted and locked so REDIM / ERASE raise error 10), BAS_REF_UDT (the TYPE instance holding the field), BAS_REF_OBJECT (the class instance holding the member variable), or BAS_REF_CELL (a BasRefCellT holding both the locked array and the TYPE instance, for a field of an array element). refType is the target's declared type (stores convert to it) and refLen the length of a STRING * n target. basValCopy and basValRelease count the owner.
     
     .h2 BasValueT
     
    @@ -486,9 +486,9 @@ formrt/formDesign.c holds what a form designer needs, shared by the DVX BASIC ID
       Function                                                  Description
       --------                                                  -----------
       basCtlIface(typeName)                                     The widget interface of a BASIC type name, or NULL.
    -  basCtlPropAt(typeName, idx, out) / basCtlPropFind(...)    A type's properties as BasCtlPropT (name, type, readable, writable, enum names): the runtime table's ("Form", "Menu" or the common one), the widget's own winning on a name, then the widget's others. The IDE's properties window uses basCtlPropFind.
    +  basCtlPropAt(typeName, idx, out) / basCtlPropFind(...)    A type's properties as BasCtlPropT (name, type, readable, writable, enum names): the runtime table's ("Form", "Menu", or the common one), the widget's own winning on a name, then the widget's others. The IDE's properties window uses basCtlPropFind.
       basCtlMethodAt / basCtlEventAt(typeName, idx)             A type's methods (the widget's, then the common ones) and events (the master BAS_EVENT_LIST's for that kind of object, then the widget's); the IDE's event list uses basCtlEventAt.
    -  basCtlEventParams(typeName, eventName)                    The parameter list of a handler of a type's event: the widget's own (WgtEventDescT params), else the master list's; typeName NULL searches every widget after the master list, BAS_UC_OBJECT ("UserControl", the object in a UserControl's own code) has BAS_UC_EVENT_LIST's alone. The IDE's handler skeletons, CtlEventParams and CtlTypeEventParams.
    +  basCtlEventParams(typeName, eventName)                    The parameter list of a handler of a type's event: the widget's own (WgtEventDescT params), else the master list's; typeName NULL searches every widget after the master list, BAS_UC_OBJECT ("UserControl", the object in a UserControl's own code) has BAS_UC_EVENT_LIST's alone. The IDE's handler skeletons, CtlEventParams, and CtlTypeEventParams.
       basCtlDefaultEvent, basCtlNamePrefix, basCtlIsContainer   As the toolbox and designer need them.
       basCtlResPath(typeName, resBase, resName, size)           The widget file and the (suffixed) resource name of its toolbox icon (BAS_CTL_RES_ICON) or tooltip (BAS_CTL_RES_NAME).
       basCtlTypeAt(rt, idx)                                     The idx-th control type rt sees (basUcIfaceVisible; rt NULL: a C host) in alphabetical order of BASIC name, NULL past the end. The IDE's toolbox and CtlTypeName list the types in this order; the registry's own (wgtIfaceAt) is the order the widget files loaded in.
    @@ -498,11 +498,11 @@ formrt/formDesign.c holds what a form designer needs, shared by the DVX BASIC ID
       basDsgnWidgetHit(w, x, y)                                 True when a point is on a visible widget.
     .endtable
     
    -Form.DesignMode replaces the form window's mouse, key, paint and cursor handlers while it is True (the controls get no input, and fireCtrlEvent drops their events), raises DesignMouse, DesignKey and DesignPropertyChanged, and paints DesignSelection's handles after the widgets. Form.Controls, Control.Parent / Container and MoveBefore (basFormRtMoveControl) are dispatched there too.
    +Form.DesignMode replaces the form window's mouse, key, paint, and cursor handlers while it is True (the controls get no input, and fireCtrlEvent drops their events), raises DesignMouse, DesignKey, and DesignPropertyChanged, and paints DesignSelection's handles after the widgets. Form.Controls, Control.Parent / Container, and MoveBefore (basFormRtMoveControl) are dispatched there too.
     
     .h2 UserControls for a Host
     
    -formrt/formCtl.c registers UserControls (widget classes written in BASIC) with the widget system. A program's own controls (basFormRtRegisterCtl, the stub and the IDE's runs) and a designer's (basCtlLoadDesign) get private registry keys, the class name plus a serial, so the same project running and being designed, or two programs, never share one; a control file's (basCtlLoadFile) keeps its name. Resolve names through these instead of wgtGetIface:
    +formrt/formCtl.c registers UserControls (widget classes written in BASIC) with the widget system. A program's own controls (basFormRtRegisterCtl: the stub's and the IDE's runs) and a designer's (basCtlLoadDesign) get private registry keys, the class name plus a serial, so the same project running and being designed, or two programs, never share one; a control file's (basCtlLoadFile) keeps its name. Resolve names through these instead of wgtGetIface:
     
     .table
       Function                                                  Description
    @@ -598,7 +598,7 @@ void    basVmSetAppPaths(BasVmT *vm, const char *path,
       basVmStop                   Stop a debugged program: step state cleared, breakpoints detached, break mode ended, the running code unwinds.
       basVmGetCurrentLine         Get the current source line number (from the last OP_LINE instruction).
       basVmGetErrorLine           The source line where the current runtime error was raised (0: none).
    -  basVmSetAppPaths            Set App.Path, App.Config and App.Data.
    +  basVmSetAppPaths            Set App.Path, App.Config, and App.Data.
     .endtable
     
     The breakpoint callback notifies the host when a breakpoint fires during nested sub calls:
    @@ -687,10 +687,13 @@ typedef struct {
     .index frmModelWrite
     .index frmModelText
     .index frmModelFromForm
    +.index basFontChoiceOf
    +.index basFontPropText
    +.index Font properties (C)
     
     .h1 The .frm Model
     
    -A form file as data, with no widgets and no windows: the form's properties, its controls (type, name, control array index, parent, the built-in size and state fields and every other "Key = Value" pair as text), its menu items and its code section. frmModelLoad reads .frm text (through frmParse) and frmModelWrite writes the canonical text the DVX BASIC IDE saves. The IDE's designer, properties window, menu editor, copy and paste all work on this model and keep only their live widgets on the side (each control's userData); designer.bas binds it for BASIC programs (the Frm* functions, formrt/frmModelBas.c).
    +A form file as data, with no widgets and no windows: the form's properties, its controls (type, name, control array index, parent, the built-in size and state fields, and every other "Key = Value" pair as text), its menu items, and its code section. frmModelLoad reads .frm text (through frmParse) and frmModelWrite writes the canonical text the DVX BASIC IDE saves. The IDE's designer, properties window, menu editor, and copy and paste all work on this model and keep only their live widgets on the side (each control's userData); designer.bas binds it for BASIC programs (the Frm* functions, formrt/frmModelBas.c).
     
     Header: apps/dvxbasic/formrt/frmModel.h (part of basrt.lib)
     
    @@ -699,14 +702,14 @@ Header: apps/dvxbasic/formrt/frmModel.h (part of basrt.lib)
     .table
       Type            Description
       ----            -----------
    -  FrmModelT       A form: name, caption, width / height / left / top, padding, spacing, windowState, layout, helpTopic, contextMenu, centered / autoSize / resizable, controls (stb_ds array of FrmCtrlT pointers in tree order: each control directly followed by its children), menuItems (stb_ds array), props (other runtime form properties that were set: Icon, Tag, MousePointer ...), code (malloc'd, NULL = none), dirty (changed by an edit since it was made or loaded).
    -  FrmCtrlT        A control: name, typeName, parentName + parentIndex (the parent's control array index, so a child of one member of a container array belongs to that member), index (FRM_NO_INDEX: not in an array), left, top, width (MinWidth), height (MinHeight), maxWidth, maxHeight, weight, dragMode, helpTopic, visible, enabled, props (stb_ds array of FrmPropT: every other key, unquoted), userData (the caller's; the model never reads it and loads, copies and pastes start it at NULL).
    +  FrmModelT       A form: name, caption, width / height / left / top, padding, spacing, windowState, layout, helpTopic, contextMenu, centered / autoSize / resizable, controls (stb_ds array of FrmCtrlT pointers in tree order: each control directly followed by its children), menuItems (stb_ds array), props (other runtime form properties that were set: Icon, Tag, MousePointer, the Font properties ...), code (malloc'd, NULL = none), dirty (changed by an edit since it was made or loaded).
    +  FrmCtrlT        A control: name, typeName, parentName + parentIndex (the parent's control array index, so a child of one member of a container array belongs to that member), index (FRM_NO_INDEX: not in an array), left, top, width (MinWidth), height (MinHeight), maxWidth, maxHeight, weight, dragMode, helpTopic, visible, enabled, props (stb_ds array of FrmPropT: every other key, unquoted), userData (the caller's; the model never reads it and loads, copies, and pastes start it at NULL).
       FrmMenuItemT    A menu item: caption ("-" for a separator), name, shortcut, shortcutText, level (0: a menu-bar menu or a popup; 1: its items; 2+: submenus), id (an editor identity, see frmModelMenuReplace), checked, checkable (Checked was written), radioCheck, enabled, visible (False on a level-0 item: a popup menu).
       FrmPropT        One stored "Key = Value": name, value.
       FrmIntPropT     One built-in integer property (Left, Top, MinWidth alias Width, MinHeight alias Height, MaxWidth, MaxHeight, Weight, DragMode): name, alias, offset into FrmCtrlT, saveRule (FRM_SAVE_ALWAYS / IF_NONZERO / IF_POSITIVE).
     .endtable
     
    -Only values somebody set are stored and written: a property a control has by default (a PictureBox's BorderStyle, a TextBox's MaxLength) is not in the model and not in the file. Values are written in the canonical form for their widget interface type (STRING quoted, BOOL True / False, INT a number, FLOAT and ENUM unquoted); keys the widget does not know are written as quoted strings. The limits come from the parser's: FRM_MAX_NEST_DEPTH control levels, FRM_MAX_MENU_LEVEL menu levels, and no property line longer than FRM_MAX_LINE_LEN - 1 characters (the writer fails with FRM_WRITE_LINE_TOO_LONG rather than write one the parser would cut).
    +Only values somebody set are stored and written: a property a control has by default (a PictureBox's BorderStyle, a TextBox's MaxLength) is not in the model and not in the file. Values are written in the canonical form for their widget interface type, or for a key the widget does not know, for their type in the runtime's common property table (STRING quoted, BOOL True / False, INT a number, FLOAT and ENUM unquoted); other keys are written as quoted strings. The limits come from the parser's: FRM_MAX_NEST_DEPTH control levels, FRM_MAX_MENU_LEVEL menu levels, and no property line longer than FRM_MAX_LINE_LEN - 1 characters (the writer fails with FRM_WRITE_LINE_TOO_LONG rather than write one the parser would cut).
     
     .h2 Loading and Writing
     
    @@ -715,13 +718,13 @@ Only values somebody set are stored and written: a property a control has by def
       --------                                      -----------
       frmModelNew(name)                             A blank form with the runtime form's defaults.
       frmModelLoad(text, len)                       The model of .frm text; NULL when it is not a form (no Begin Form, frmParse refuses it, two controls share a name and index) or out of memory. A Key = Value line is the innermost open block's, also after the blocks inside it, as the runtime reads it; a child names its container by the container's final Index. Of several OptionButtons of one container saved with Value True, the last keeps it.
    -  frmModelFree(m)                               Frees the model, its controls and menus.
    +  frmModelFree(m)                               Frees the model, its controls, and menus.
       frmModelWrite(m, buf, bufSize)                The .frm text into buf: its length, or FRM_WRITE_TOO_LARGE / FRM_WRITE_LINE_TOO_LONG (never a partial text).
       frmModelWriteSize(m)                          A buffer size that always holds the text.
       frmModelText(m, &len)                         The text in a new malloc'd string; NULL with len set to the FRM_WRITE_* code.
       frmModelSetCode(m, code)                      Replaces the code section.
       frmCodeSection(trailing, len, &outLen)        The code section of the text after a form's End (blank lines skipped, the first code line keeps its indent).
    -  frmModelFromForm(rt, form)                    A model of a loaded form as it is now: its properties, its controls in their containers with each property that differs from a new control of the type (compared with one made in a hidden window), its menus; no code. What FormText$ writes.
    +  frmModelFromForm(rt, form)                    A model of a loaded form as it is now: its properties, its controls in their containers with each property that differs from a new control of the type (compared with one made in a hidden window; the six Font properties of the form and of each control that has a font of its own), its menus; no code. What FormText$ writes.
     .endtable
     
     .h2 Properties
    @@ -729,7 +732,7 @@ Only values somebody set are stored and written: a property a control has by def
     .table
       Function                                              Description
       --------                                              -----------
    -  frmModelFormProp(m, name, out, size)                  A form property as text (an unset design property gives "", "0" or "False"); false for run-time-only ones (Visible, DesignMode, DesignSelection, object properties).
    +  frmModelFormProp(m, name, out, size)                  A form property as text (an unset design property gives "", "0", or "False"); false for run-time-only ones (Visible, DesignMode, DesignSelection, object properties).
       frmModelSetFormProp(m, name, value)                   Sets one from text; Name renames the form; an empty value takes an optional one out.
       frmCtrlPropText(ctrl, name, out, size)                A control property as the control holds it (Name, Index, the built-in integers, HelpTopic, Visible / Enabled unless the widget defines them, the stored ones).
       frmCtrlSetPropText(ctrl, key, value)                  Applies one the way the loader does (a VB TabIndex is dropped unless the widget defines it).
    @@ -739,6 +742,20 @@ Only values somebody set are stored and written: a property a control has by def
       frmIfaceProp(typeName, propName)                      The widget interface property of a type, NULL when none.
     .endtable
     
    +.h2 Font Properties
    +
    +FontName, FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru (BAS_PROP_FONT*) are in both the form table and the common control table. They are one font, a DvxFontChoiceT (dvxDlg.h: family, size in pixels, FONT_STYLE_* flags), and the widget's font (wgtFont, wgtSetFont) is the only place they live: a read gives the font the object draws with (a form's is its root widget's), a write gives it that font with the one part changed. The .frm loader collects a block's Font properties and opens one font when the block's properties are read, so their order in the file does not matter. Helpers in formrt.h, for loaders and designers that hold them as .frm text:
    +
    +.table
    +  Function                                        Description
    +  --------                                        -----------
    +  basFontPropAt(idx)                              The Font property names in .frm order (FontName, FontSize, then the flags), NULL past the end.
    +  basFontPropIs(name)                             True for a Font property name (any case).
    +  basFontChoiceOf(font, out)                      A BitmapFontT as the properties name it (dvxFontFamilyOf, pixelSize, style).
    +  basFontPropText(choice, name, out, size)        One property of a choice as .frm text (the family, the size, True / False).
    +  basFontPropSetText(choice, name, text)          Sets one property of a choice from .frm text.
    +.endtable
    +
     .h2 Editing Controls
     
     .table
    @@ -749,7 +766,7 @@ Only values somebody set are stored and written: a property a control has by def
       frmModelRenameCtrl(m, oldName, newName)                   Renames a whole control array; children and DataSource / MasterSource of a renamed Data control follow. Identifier rules are the caller's.
       frmModelMakeArrayMember(m, ctrl, index)                   Gives a control array index; its children follow.
       frmModelMoveCtrl(m, ctrl, before)                         Moves ctrl with its subtree before a sibling (NULL: after the last).
    -  frmModelReparent(m, ctrl, parent)                         Makes ctrl the last child of another container (refuses cycles, non-containers and too deep a nesting).
    +  frmModelReparent(m, ctrl, parent)                         Makes ctrl the last child of another container (refuses cycles, non-containers, and too deep a nesting).
       frmModelSibling(m, idx, dir)                              The sibling next to a control (+1 after its subtree, -1 before).
       frmModelFindCtrl / CtrlPos / FindParent / CtrlDepth       Lookups.
       frmModelIsDescendant / SubtreeEnd / NormalizeOrder        The tree order.
    diff --git a/src/apps/kpunch/dvxbasic/compiler/front.h b/src/apps/kpunch/dvxbasic/compiler/front.h
    index 1a27c65c..2cfaa695 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/front.h
    +++ b/src/apps/kpunch/dvxbasic/compiler/front.h
    @@ -134,7 +134,7 @@
     #define BAS_MSGBOX_OK_ONLY     0x0000
     
     // The error for a name after AS that is no type.
    -#define BAS_TYPE_EXPECTED      "Expected type name (Integer, Long, Single, Double, String, Boolean, Date, Variant, Control, Form, Object, Collection, TYPE, ENUM or class name)"
    +#define BAS_TYPE_EXPECTED      "Expected type name (Integer, Long, Single, Double, String, Boolean, Date, Variant, Control, Form, Object, Collection, TYPE, ENUM, or class name)"
     
     // Types named by identifiers rather than reserved words.
     #define BAS_DATE_TYPE_NAME       "DATE"
    diff --git a/src/apps/kpunch/dvxbasic/compiler/parseDecl.c b/src/apps/kpunch/dvxbasic/compiler/parseDecl.c
    index 6fdacc75..2fd1ba9c 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/parseDecl.c
    +++ b/src/apps/kpunch/dvxbasic/compiler/parseDecl.c
    @@ -291,7 +291,7 @@ BasNodeT *basParseEvent(BasCompilerT *c) {
     }
     
     
    -// True at "Friend" followed by SUB, FUNCTION or PROPERTY (FRIEND is not a
    +// True at "Friend" followed by SUB, FUNCTION, or PROPERTY (FRIEND is not a
     // reserved word).
     bool basParseFriendAhead(const BasCompilerT *c) {
         if (!basParseCheckWord(c, BAS_KW_FRIEND)) {
    @@ -1397,7 +1397,7 @@ static BasNodeT *parseProcDef(BasCompilerT *c, uint8_t memberKind, bool isPrivat
         }
     
         if (c->blockDepth > BAS_UNIT_BLOCK_DEPTH) {
    -        basParseError(c, "%s cannot be defined inside IF, FOR, DO, WHILE, SELECT CASE or WITH", kwName);
    +        basParseError(c, "%s cannot be defined inside IF, FOR, DO, WHILE, SELECT CASE, or WITH", kwName);
             return NULL;
         }
     
    @@ -1471,7 +1471,7 @@ static BasNodeT *parsePropertyDef(BasCompilerT *c, bool isPrivate) {
             return parseProcDef(c, BAS_MEMBER_SET, isPrivate);
         }
     
    -    basParseErrorExpected(c, "GET, LET or SET after PROPERTY");
    +    basParseErrorExpected(c, "GET, LET, or SET after PROPERTY");
         return NULL;
     }
     
    diff --git a/src/apps/kpunch/dvxbasic/compiler/parseExpr.c b/src/apps/kpunch/dvxbasic/compiler/parseExpr.c
    index 83a08a84..d44f6605 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/parseExpr.c
    +++ b/src/apps/kpunch/dvxbasic/compiler/parseExpr.c
    @@ -853,7 +853,7 @@ static BasNodeT *parseKeywordFunc(BasCompilerT *c) {
                     return basParseBuiltinCall(c, BAS_BUILTIN_ERR_SOURCE, NULL, 0, line, col);
                 }
     
    -            basParseError(c, "Err has no value '%s' (Number, Description and Source; Raise and Clear are statements)", member != NULL ? member : "");
    +            basParseError(c, "Err has no value '%s' (Number, Description, and Source; Raise and Clear are statements)", member != NULL ? member : "");
                 return NULL;
             }
     
    @@ -1183,7 +1183,7 @@ static BasNodeT *parsePrimary(BasCompilerT *c) {
                 basParseAdvance(c);
     
                 if (!basParseCheck(c, TOK_IDENT) || (basClassFind(c, c->lex.token.text) < 0 && strcasecmp(c->lex.token.text, BAS_COLLECTION_TYPE_NAME) != 0 && !basIsFormName(c, c->lex.token.text))) {
    -                basParseErrorExpected(c, "class name, form name or Collection after New");
    +                basParseErrorExpected(c, "class name, form name, or Collection after New");
                     return NULL;
                 }
     
    diff --git a/src/apps/kpunch/dvxbasic/compiler/parseStmt.c b/src/apps/kpunch/dvxbasic/compiler/parseStmt.c
    index 06d306a9..e4c89900 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/parseStmt.c
    +++ b/src/apps/kpunch/dvxbasic/compiler/parseStmt.c
    @@ -1443,7 +1443,7 @@ static BasNodeT *parseExit(BasCompilerT *c) {
         BasTokenTypeE what = c->lex.token.type;
     
         if (what != TOK_FOR && what != TOK_DO && what != TOK_PROPERTY && what != TOK_SUB && what != TOK_FUNCTION) {
    -        basParseError(c, "Expected FOR, DO, SUB, FUNCTION or PROPERTY after EXIT");
    +        basParseError(c, "Expected FOR, DO, SUB, FUNCTION, or PROPERTY after EXIT");
             return NULL;
         }
     
    @@ -2757,7 +2757,7 @@ static BasNodeT *parseScreenStatement(BasCompilerT *c, const ScreenStmtT *stmt)
                     argAdd(&args, basParseIntLit(c, basParseCheckWord(c, BAS_KW_STOP) ? BAS_PEN_STOP : BAS_PEN_OFF));
                     basParseAdvance(c);
                 } else {
    -                basParseErrorExpected(c, "ON, OFF or STOP after PEN");
    +                basParseErrorExpected(c, "ON, OFF, or STOP after PEN");
                     return NULL;
                 }
     
    diff --git a/src/apps/kpunch/dvxbasic/compiler/resolve.c b/src/apps/kpunch/dvxbasic/compiler/resolve.c
    index 8afab593..8c58385e 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/resolve.c
    +++ b/src/apps/kpunch/dvxbasic/compiler/resolve.c
    @@ -1326,7 +1326,7 @@ static bool buildSignature(BasCompilerT *c, const BasProcT *proc, uint32_t flags
     
             if (p->defExpr != NULL) {
                 if (!(p->flags & PF_OPTIONAL) || (p->flags & PF_ARRAY) || dt == BAS_TYPE_UDT) {
    -                basErrorNode(c, p->defExpr, "Only an Optional parameter of a number, string, date, object or Variant type can have a default value");
    +                basErrorNode(c, p->defExpr, "Only an Optional parameter of a number, string, date, object, or Variant type can have a default value");
                     return false;
                 }
     
    @@ -2163,7 +2163,7 @@ static void resolveAssign(BasCompilerT *c, BasStmtT *s) {
     // emit (then a call of Class_Initialize) is the instance set-up New runs.
     static void resolveBeginClass(BasCompilerT *c, BasStmtT *s) {
         if (c->inClass || c->inForm || c->inProc) {
    -        basErrorNode(c, &s->h, "BEGINCLASS inside a form, class or procedure is not allowed");
    +        basErrorNode(c, &s->h, "BEGINCLASS inside a form, class, or procedure is not allowed");
             return;
         }
     
    @@ -2429,12 +2429,12 @@ static void resolveDeclarator(BasCompilerT *c, BasDeclT *d, bool isShared, bool
         bool newForm = asNew && d->spec->name != NULL && basIsFormName(c, d->spec->name);
     
         if (asNew && ((objClass == BAS_CLASS_NONE && !newForm) || d->isArray)) {
    -        basErrorNode(c, &d->h, d->isArray ? "New cannot be used with an array" : "New needs a class name, a form name or Collection");
    +        basErrorNode(c, &d->h, d->isArray ? "New cannot be used with an array" : "New needs a class name, a form name, or Collection");
             return;
         }
     
         if (d->withEvents && c->inProc) {
    -        basErrorNode(c, &d->h, "WithEvents variables belong at module level of a module, form or class");
    +        basErrorNode(c, &d->h, "WithEvents variables belong at module level of a module, form, or class");
             return;
         }
     
    @@ -2677,7 +2677,7 @@ static void resolveDeclStatement(BasCompilerT *c, BasNodeT *n) {
     
             case ST_BEGINMODULE:
                 if (c->fileIdx != 0 || c->inForm || c->inClass || c->inProc) {
    -                basErrorNode(c, n, "BEGINMODULE inside a module, form, class or procedure is not allowed");
    +                basErrorNode(c, n, "BEGINMODULE inside a module, form, class, or procedure is not allowed");
                     return;
                 }
     
    diff --git a/src/apps/kpunch/dvxbasic/ctrlover.dhs b/src/apps/kpunch/dvxbasic/ctrlover.dhs
    index bbeb30ec..f3450096 100644
    --- a/src/apps/kpunch/dvxbasic/ctrlover.dhs
    +++ b/src/apps/kpunch/dvxbasic/ctrlover.dhs
    @@ -51,6 +51,13 @@
     .index vbSizeNESW
     .index vbNoDrop
     .index ScrollToControl
    +.index FontName
    +.index FontSize
    +.index FontBold
    +.index FontItalic
    +.index FontUnderline
    +.index FontStrikethru
    +.index Fonts (controls)
     
     .h1 Common Properties, Events, and Methods
     
    @@ -75,18 +82,28 @@ Every control in DVX BASIC inherits a set of common properties, events, and meth
       Enabled      Boolean   R/W   Whether the control accepts user input. Disabling a container (Frame, VBox, HBox...) also blocks mouse and keyboard input to every control inside it.
       BackColor    Long      R/W   Background color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
       ForeColor    Long      R/W   Foreground (text) color as a 24-bit RGB value packed in a Long (use the RGB function to construct).
    +  FontName     String    R/W   The family of the font the control draws its text in, such as "DejaVu Sans" ("System" is DVX's own 8 x 16 font). A family DVX does not have gives the System font. See Fonts below.
    +  FontSize     Integer   R/W   The size of the font in pixels. A size the family does not come in becomes the nearest one it has.
    +  FontBold     Boolean   R/W   True for the bold style of the font.
    +  FontItalic   Boolean   R/W   True for the italic style of the font.
    +  FontUnderline Boolean  R/W   True to underline the text.
    +  FontStrikethru Boolean R/W   True to draw a line through the text.
       TabIndex     Integer   R     Accepted for VB compatibility but ignored. DVX has no tab order.
       ToolTipText  String    R/W   Tooltip text shown after the mouse hovers over the control. Empty string or unset = no tooltip.
       HelpTopic    String    R/W   Help topic opened by F1 while the control has the focus. Empty = the form's HelpTopic. F1 uses the project's help file.
       DragMode     Integer   R/W   vbManual (0, the default): the program starts a drag with the Drag method. vbAutomatic (1): pressing the left mouse button on the control starts a drag. See Drag and Drop below.
    -  DragIcon     Picture   R/W   Picture shown under the mouse while the control is dragged: a picture file name (BMP, PNG, JPEG or GIF) or a Picture object from LOADPICTURE (Set works too). Reading it gives a Picture object, which stands for the file name where a value is needed. Empty = a dotted outline the size of the control.
    +  DragIcon     Picture   R/W   Picture shown under the mouse while the control is dragged: a picture file name (BMP, PNG, JPEG, or GIF) or a Picture object from LOADPICTURE (Set works too). Reading it gives a Picture object, which stands for the file name where a value is needed. Empty = a dotted outline the size of the control.
       Parent       Form      R     The form the control is on (Text1.Parent.Caption).
       Tag          String    R/W   Any text the program wants to keep with the control (DVX does nothing with it), as in Visual Basic. Also in the .frm file.
    -  MousePointer Integer   R/W   The mouse pointer shown over the control (and the controls inside it that have none of their own): vbDefault (0, the control's own pointer), vbArrow (1), vbCrosshair (2), vbSizeNESW (6), vbSizeNS (7), vbSizeNWSE (8), vbSizeWE (9), vbHourglass (11) or vbNoDrop (12). A form's MousePointer is used where no control has one, and Screen.MousePointer over every form of the program. Change it in MouseMove to show different pointers over parts of a control. Also in the .frm file; another value is an error.
    +  MousePointer Integer   R/W   The mouse pointer shown over the control (and the controls inside it that have none of their own): vbDefault (0, the control's own pointer), vbArrow (1), vbCrosshair (2), vbSizeNESW (6), vbSizeNS (7), vbSizeNWSE (8), vbSizeWE (9), vbHourglass (11), or vbNoDrop (12). A form's MousePointer is used where no control has one, and Screen.MousePointer over every form of the program. Change it in MouseMove to show different pointers over parts of a control. Also in the .frm file; another value is an error.
       Container    Object    R/W   The container control the control is in (a Frame, VBox, TabPage ...), or its form. Set Text1.Container = Frame1 moves the control into Frame1, after the controls there; Set Text1.Container = Me moves it onto the form itself. The new container must be a container control on the same form, not the control itself or one inside it.
     .endtable
     
    -Size properties (Width, Height and the Min/Max sizes) are limited to 0..32767.
    +Size properties (Width, Height, and the Min/Max sizes) are limited to 0..32767.
    +
    +.h2 Fonts
    +
    +The six Font properties are one font. A control that has not been given one draws in the font of the control it is in (a Frame, for example), or else in its form's; the properties read the font the control draws with. Setting one of them gives the control a font of its own with that one part changed, keeping the others: Label1.FontBold = True makes the label's text bold in whatever family and size it had. Controls that edit text in columns (TextBox, TextArea, ComboBox, SpinButton, and Terminal) take only fixed-pitch families, such as "DejaVu Sans Mono", and show the System font in place of any other. The fonts DVX has, and the sizes each comes in, are listed by font.bas; DlgFont in commdlg.bas lets the user choose one. In the form designer the Font row of the Properties window sets all six, and they are saved in the .frm file.
     
     .h2 Default Properties
     
    @@ -103,7 +120,7 @@ As in Visual Basic, a control's name used where a value is needed means its defa
       Timer                                                 Enabled
     .endtable
     
    -Other controls, forms and menu items have no default property: using one as a value raises error 13.
    +Other controls, forms, and menu items have no default property: using one as a value raises error 13.
     
     .h2 Common Events
     
    @@ -118,7 +135,7 @@ These events are wired on every control, whether it is loaded from a .frm file o
       GotFocus    (none)                                        Fires when the control receives keyboard focus.
       LostFocus   (none)                                        Fires when the control loses keyboard focus.
       KeyPress    KeyAscii As Integer                           Fires when a printable key is pressed. KeyAscii is the ASCII code.
    -  KeyDown     KeyCode As Integer, Shift As Integer          Fires when any key is pressed down, after the control has handled it. KeyCode is the key (vbKeyDelete, vbKeyReturn, ... or a character's code; Ctrl+A to Ctrl+Z are 1 to 26); Shift holds vbShiftMask, vbCtrlMask and vbAltMask bits (see Key Code Constants in the language reference).
    +  KeyDown     KeyCode As Integer, Shift As Integer          Fires when any key is pressed down, after the control has handled it. KeyCode is the key (vbKeyDelete, vbKeyReturn, ... or a character's code; Ctrl+A to Ctrl+Z are 1 to 26); Shift holds vbShiftMask, vbCtrlMask, and vbAltMask bits (see Key Code Constants in the language reference).
       KeyUp       KeyCode As Integer, Shift As Integer          Fires when a key is released.
       MouseDown   Button As Integer, X As Integer, Y As Integer Fires when a mouse button is pressed over the control.
       MouseUp     Button As Integer, X As Integer, Y As Integer Fires when a mouse button is released over the control.
    @@ -146,7 +163,7 @@ Any control can be dragged onto another control or onto a form, as in Visual Bas
     
     The mouse pointer shows where a drop can land: over a control or form that has a DragDrop event procedure it is the usual arrow; over one without, and outside the program's forms, it is the "no drop" sign (a circle with a line through it). The outline or DragIcon picture still follows the mouse either way.
     
    -With DragMode set to vbAutomatic, pressing the left mouse button on the control starts the drag, and the control gets no Click, MouseDown or MouseUp from the left button. With DragMode vbManual, start the drag yourself, usually in MouseDown. A drag started while the mouse button is up ends at the next click. During a drag no mouse events other than DragOver and DragDrop are raised.
    +With DragMode set to vbAutomatic, pressing the left mouse button on the control starts the drag, and the control gets no Click, MouseDown, or MouseUp from the left button. With DragMode vbManual, start the drag yourself, usually in MouseDown. A drag started while the mouse button is up ends at the next click. During a drag no mouse events other than DragOver and DragDrop are raised.
     
     Drop targets only react: nothing is moved or copied for you. Use Source in DragDrop to decide what to do, for example read its Name or Caption, or check what kind of control it is with TypeOf Source Is TextBox (see Operators).
     
    @@ -231,7 +248,7 @@ End Sub
     
     Forms and PictureBoxes have Visual Basic's drawing methods. They draw with the same code as the QBasic graphics statements (see Console and Graphics in the language reference), in pixels unless ScaleMode or Scale says otherwise, with colours as Visual Basic RGB values, &HBBGGRR (RGB(255, 0, 0) or &HFF is red). A PictureBox draws on its picture; a form gets a drawing area the size of its window the first time it draws, placed with its controls like a PictureBox (so draw on a form that has no controls, or use a PictureBox beside them).
     
    -Drawings stay until they are drawn over or cleared. In a form's own code, write Me.Line, Me.Circle and so on: Line, Circle and PSet without an object are the QBasic graphics statements (they draw on the program's screen), and Print without an object is the PRINT statement.
    +Drawings stay until they are drawn over or cleared. In a form's own code, write Me.Line, Me.Circle, and so on: Line, Circle, and PSet without an object are the QBasic graphics statements (they draw on the program's screen), and Print without an object is the PRINT statement.
     
     .h2 Drawing Methods
     
    @@ -240,17 +257,17 @@ Drawings stay until they are drawn over or cleared. In a form's own code, write
       ------                                                          -----------
       Cls                                                             Clears the drawing to BackColor; CurrentX and CurrentY go to 0.
       PSet [Step](x, y) [, color]                                     Draws a point (DrawWidth wide).
    -  Line [[Step](x1, y1)]-[Step](x2, y2) [, color] [, B | BF]         Draws a line, a box (B) or a filled box (BF). A box without F is filled with FillColor when FillStyle is 0.
    -  Circle [Step](x, y), radius [, color [, start [, end [, aspect]]]]  Draws a circle, ellipse or arc, filled with FillColor when FillStyle is 0 (full circles).
    +  Line [[Step](x1, y1)]-[Step](x2, y2) [, color] [, B | BF]         Draws a line, a box (B), or a filled box (BF). A box without F is filled with FillColor when FillStyle is 0.
    +  Circle [Step](x, y), radius [, color [, start [, end [, aspect]]]]  Draws a circle, ellipse, or arc, filled with FillColor when FillStyle is 0 (full circles).
       DrawFocusRect x, y, width, height [, color]                     Draws the dotted rectangle DVX shows around the control that has the keyboard focus (every other pixel of its edge), for things a program draws itself, such as the focused link of a help topic. A negative width or height counts from the other edge.
    -  Point(x, y)                                                     The colour of a pixel, -1 outside the picture. With fewer than 16 million colours on the screen it is the colour the screen shows: black, white and full colours such as RGB(255, 0, 0) read back exactly, others may differ slightly.
    -  Print items                                                     Writes text at CurrentX, CurrentY in ForeColor, FontScale times as large, with PRINT's ; , SPC and TAB; ends by moving to the next line unless the last item is followed by ; or ,.
    -  PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight [, opcode]]]   Copies a picture, stretched to width x height: an image file (BMP, PNG, JPEG or GIF), a picture from LOADPICTURE, or a PictureBox given by its name as a string or by a Control variable. opcode combines each pixel with the one already there (see Raster Operations).
    +  Point(x, y)                                                     The colour of a pixel, -1 outside the picture. With fewer than 16 million colours on the screen it is the colour the screen shows: black, white, and full colours such as RGB(255, 0, 0) read back exactly, others may differ slightly.
    +  Print items                                                     Writes text at CurrentX, CurrentY in ForeColor, in the object's font FontScale times as large, with PRINT's separators (; and ,), SPC, and TAB (counted in the font's character width); ends by moving to the next line unless the last item is followed by ; or ,.
    +  PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight [, opcode]]]   Copies a picture, stretched to width x height: an image file (BMP, PNG, JPEG, or GIF), a picture from LOADPICTURE, or a PictureBox given by its name as a string or by a Control variable. opcode combines each pixel with the one already there (see Raster Operations).
       Scale [(x1, y1)-(x2, y2)]                                        Gives the drawing coordinates of its own: (x1, y1) is the top-left corner and (x2, y2) the bottom-right one (y2 below y1 makes y go up). Scale alone goes back to pixels.
       ScaleX(width [, fromScale [, toScale]])                         A width in fromScale converted to toScale (see Scales).
       ScaleY(height [, fromScale [, toScale]])                        A height in fromScale converted to toScale.
    -  TextWidth(text)                                                 Width of the text in the scale's units, at the FontScale size.
    -  TextHeight(text)                                                Height of a line of text in the scale's units, at the FontScale size.
    +  TextWidth(text)                                                 Width of the text in the object's font, in the scale's units, at the FontScale size.
    +  TextHeight(text)                                                Height of a line of text in the object's font, in the scale's units, at the FontScale size.
     .endtable
     
     .h2 Drawing the DVX Look
    @@ -262,13 +279,13 @@ These methods draw the parts DVX's own controls are made of, in the colours of t
       ------                                                          -----------
       DrawBevel x, y, width, height [, sunken [, face [, border]]]    A raised box like a window or button edge, or with sunken True a sunken one like a text box's. face is the fill colour (the theme's face colour when left out; -1 leaves the inside as it is, to frame what is already drawn); border is the edge width in pixels (2).
       DrawButtonFace x, y, width, height [, pressed [, face]]         A button face, up or with pressed True pushed down.
    -  DrawArrow x, y, size, direction [, color]                       A solid triangle centred on x, y, size pixel rows tall, pointing dvxArrowUp (0), dvxArrowDown (1), dvxArrowLeft (2) or dvxArrowRight (3), in ForeColor unless a colour is given.
    +  DrawArrow x, y, size, direction [, color]                       A solid triangle centred on x, y, size pixel rows tall, pointing dvxArrowUp (0), dvxArrowDown (1), dvxArrowLeft (2), or dvxArrowRight (3), in ForeColor unless a colour is given.
       DrawScrollBar x, y, length, position, visible, total [, horizontal [, control]]   A scroll bar with its arrows and its thumb, for a list of total rows of which visible are shown from row position, with its top-left corner at x, y. It stands upright (16 pixels wide, length tall, arrows at the top and bottom) unless horizontal is True: then it lies across (length wide, 16 pixels tall, arrows at the left and right ends). With control True it is the narrower bar of a control such as ListBox (14 pixels).
    -  DrawScrollArrow x, y, size, direction                           One arrow button of a scroll bar, as at the ends of DrawScrollBar's bars: a square button size wide and tall (size in the scale's horizontal units) with its top-left corner at x, y and an arrow pointing dvxArrowUp, dvxArrowDown, dvxArrowLeft or dvxArrowRight, in the theme's scroll bar colours.
    -  ScrollBarClick(length, offset, position, visible, total [, horizontal [, control]])   The new position after a click on a scroll bar DrawScrollBar drew with the same length, rows, direction and kind: offset is how far the click is from the top of the bar, or from its left end when horizontal is True. The arrows move one row, the trough a page, the thumb leaves it as it is. Needs no drawing.
    -  ScrollBarsNeeded(contentWidth, contentHeight, areaWidth, areaHeight)   Which scroll bars content that big needs in an area that big, as DVX's controls decide it: vbSBNone (0), vbHorizontal (1), vbVertical (2) or vbBoth (3). A vertical bar narrows the area and may make a horizontal one needed, and the other way round. The sizes are in the object's scale. Needs no drawing.
    -  PrintAccel text [, enabled]                                     Prints a caption at CurrentX, CurrentY with the letter after & underlined (&& prints one &); enabled False gives the greyed, embossed look of a disabled control. CurrentX moves past the text.
    -  TextWidthAccel(text)                                            Width of a caption without its & signs, in the scale's units.
    +  DrawScrollArrow x, y, size, direction                           One arrow button of a scroll bar, as at the ends of DrawScrollBar's bars: a square button size wide and tall (size in the scale's horizontal units) with its top-left corner at x, y and an arrow pointing dvxArrowUp, dvxArrowDown, dvxArrowLeft, or dvxArrowRight, in the theme's scroll bar colours.
    +  ScrollBarClick(length, offset, position, visible, total [, horizontal [, control]])   The new position after a click on a scroll bar DrawScrollBar drew with the same length, rows, direction, and kind: offset is how far the click is from the top of the bar, or from its left end when horizontal is True. The arrows move one row, the trough a page, the thumb leaves it as it is. Needs no drawing.
    +  ScrollBarsNeeded(contentWidth, contentHeight, areaWidth, areaHeight)   Which scroll bars content that big needs in an area that big, as DVX's controls decide it: vbSBNone (0), vbHorizontal (1), vbVertical (2), or vbBoth (3). A vertical bar narrows the area and may make a horizontal one needed, and the other way round. The sizes are in the object's scale. Needs no drawing.
    +  PrintAccel text [, enabled]                                     Prints a caption in the object's font at CurrentX, CurrentY with the letter after & underlined (&& prints one &); enabled False gives the greyed, embossed look of a disabled control. CurrentX moves past the text.
    +  TextWidthAccel(text)                                            Width of a caption without its & signs in the object's font, in the scale's units.
       AccelKey(text)                                                  The access key of a caption in lower case ("f" for "&File"), "" when it has none.
     .endtable
     
    @@ -291,7 +308,7 @@ Sub Picture1_MouseDown(Button As Integer, X As Integer, Y As Integer)
     End Sub
     .endcode
     
    -Without a colour, drawing uses ForeColor. Step makes a point relative to CurrentX, CurrentY, which every drawing method leaves at its last point. Coordinates, radii, CurrentX and CurrentY and PaintPicture's rectangles are in the scale's units (a radius in its horizontal units); DrawWidth is always in pixels. A colour or angle out of range stops the program with an error.
    +Without a colour, drawing uses ForeColor. Step makes a point relative to CurrentX, CurrentY, which every drawing method leaves at its last point. Coordinates, radii, CurrentX and CurrentY, and PaintPicture's rectangles are in the scale's units (a radius in its horizontal units); DrawWidth is always in pixels. A colour or angle out of range stops the program with an error.
     
     .h2 Raster Operations
     
    @@ -328,9 +345,9 @@ Picture1.PaintPicture Picture2, 0, 0, 32, 32, 0, 0, 32, 32, vbSrcInvert   ' and
     
     .h2 Scales
     
    -ScaleMode picks the units: 0 user (set by Scale or the Scale properties), 1 twips (1440 an inch, 15 a pixel), 2 points (72 an inch), 3 pixels, 4 characters (120 twips across, 240 down), 5 inches, 6 millimetres, 7 centimetres. Unlike Visual Basic, whose default is twips, DVX starts every form and PictureBox in pixels (ScaleMode 3), and Scale without arguments goes back to pixels. Setting ScaleLeft, ScaleTop, ScaleWidth or ScaleHeight makes a user scale from the current one; ScaleWidth and ScaleHeight may not be 0. With a user scale the units stay fixed to the picture's edges when it is resized; other scale modes keep their unit size.
    +ScaleMode picks the units: 0 user (set by Scale or the Scale properties), 1 twips (1440 an inch, 15 a pixel), 2 points (72 an inch), 3 pixels, 4 characters (120 twips across, 240 down), 5 inches, 6 millimetres, 7 centimetres. Unlike Visual Basic, whose default is twips, DVX starts every form and PictureBox in pixels (ScaleMode 3), and Scale without arguments goes back to pixels. Setting ScaleLeft, ScaleTop, ScaleWidth, or ScaleHeight makes a user scale from the current one; ScaleWidth and ScaleHeight may not be 0. With a user scale the units stay fixed to the picture's edges when it is resized; other scale modes keep their unit size.
     
    -The constants vbUser (0), vbTwips (1), vbPoints (2), vbPixels (3), vbCharacters (4), vbInches (5), vbMillimeters (6) and vbCentimeters (7) name the scale modes. The units assume a standard display of 96 pixels an inch (15 twips a pixel), whatever the screen really is, so an inch is always 96 pixels.
    +The constants vbUser (0), vbTwips (1), vbPoints (2), vbPixels (3), vbCharacters (4), vbInches (5), vbMillimeters (6), and vbCentimeters (7) name the scale modes. The units assume a standard display of 96 pixels an inch (15 twips a pixel), whatever the screen really is, so an inch is always 96 pixels.
     
     ScaleX and ScaleY convert a width or a height from one scale to another, as in Visual Basic: fromScale is the scale the value is in, vbHimetric (8, hundredths of a millimetre, the unit of a Picture's Width and Height) when it is left out; toScale the scale wanted, the object's ScaleMode when left out. vbUser means the object's current scale. Visual Basic's vbContainerPosition and vbContainerSize are not available; any other scale number stops the program with an error.
     
    @@ -367,7 +384,7 @@ End Sub
       DrawWidth     Integer   1         Pen width in pixels.
       FillStyle     Integer   1         How boxes (B) and circles are filled with FillColor: 0 solid, 1 (the default) not at all, 2 horizontal lines, 3 vertical lines, 4 upward diagonals, 5 downward diagonals, 6 a cross hatch, 7 a diagonal cross hatch (the lines 8 pixels apart, the gaps left as they are). Other values are an error. BF fills solid in the line's colour.
       FillColor     Long      &H000000  Colour boxes and circles are filled with.
    -  FontScale     Integer   1         How many times as large Print writes its text (1 to 8): each dot of DVX's 8 x 16 font becomes a FontScale x FontScale block, for titles and banners. TextWidth and TextHeight follow it; PrintAccel does not.
    +  FontScale     Integer   1         How many times as large Print writes its text (1 to 8): each dot of the font becomes a FontScale x FontScale block, for titles and banners. TextWidth and TextHeight follow it; PrintAccel does not.
       AutoRedraw    Boolean   False     True: no Paint events (see above).
       ScaleMode     Integer   3         Units of the coordinates (see Scales).
       ScaleLeft     Single    0         Coordinate of the left edge.
    @@ -553,13 +570,13 @@ In the .frm file a menu item takes these keys:
       Caption      String    The text displayed. Use & for accelerator key. Set to "-" for a separator.
       Checked      Boolean   Whether the menu item shows a checkmark. An item with a Checked line (True or False) is a check item: choosing it turns its checkmark on or off before its Click event runs, so the handler reads the new state.
       RadioCheck   Boolean   Show the check as a radio dot instead of a checkmark. Radio items next to each other form a group: choosing one checks it and clears the others.
    -  Shortcut     String    A key that runs the item from anywhere in the form, shown at the right of the item: "F2", "Ctrl+X", "Alt+Enter", "Shift+Del" (Ctrl, Alt and Shift, then a letter, digit, F1 to F12, or Backspace, Del, Down, End, Enter, Esc, Home, Ins, Left, PgDn, PgUp, Right, Space, Tab, Up).
    +  Shortcut     String    A key that runs the item from anywhere in the form, shown at the right of the item: "F2", "Ctrl+X", "Alt+Enter", "Shift+Del" (Ctrl, Alt, and Shift, then a letter, digit, F1 to F12, or Backspace, Del, Down, End, Enter, Esc, Home, Ins, Left, PgDn, PgUp, Right, Space, Tab, Up).
       ShortcutText String    Key text shown at the right of the item without making a shortcut, for keys a control handles itself (for example "Del" on a Delete item whose list deletes on Del in its KeyDown handler, so a text box keeps its own Del). Shown instead of the Shortcut's text when both are set.
       Enabled      Boolean   Whether the menu item is enabled (default True).
       Visible      Boolean   On a top-level menu, False makes it a popup menu (for ContextMenu) instead of a menu bar entry. On an item or separator inside a menu, False hides it until code sets Visible = True.
     .endtable
     
    -A Shortcut key works while the form's window has the focus, before the focused control sees the key, so give one only to keys no control there needs (F-keys, Ctrl or Alt combinations). F1 always opens the help and never reaches a shortcut. An unknown Shortcut stops the program with an error when the form loads.
    +A Shortcut key works while the form's window has the focus, before the focused control sees the key, so give one only to keys no control there needs (F-keys, Ctrl, or Alt combinations). F1 always opens the help and never reaches a shortcut. An unknown Shortcut stops the program with an error when the form loads.
     
     .code
     Begin Menu mnuRename
    @@ -581,7 +598,7 @@ At run time code can use only these properties of a menu item:
       Caption    String    R/W   The item's text, with & before its access key. Setting it keeps the item's key text (Shortcut or ShortcutText), and reading it gives the text without it.
       Checked    Boolean   R/W   Whether the menu item shows a checkmark. Setting it on an item that has neither Checked nor RadioCheck in the .frm makes it a check item (as in VB, any item can be checked).
       Enabled    Boolean   R/W   Whether the menu item is enabled.
    -  Visible    Boolean   R/W   Whether the item (or separator) is in its menu. A hidden item keeps its caption, checkmark and place: shown again, it returns where it was. A submenu cannot be hidden (hide or remove its items, or set Enabled = False).
    +  Visible    Boolean   R/W   Whether the item (or separator) is in its menu. A hidden item keeps its caption, checkmark, and place: shown again, it returns where it was. A submenu cannot be hidden (hide or remove its items, or set Enabled = False).
       Shortcut   String    R/W   The key that runs the item ("Ctrl+K", written as in the .frm file, "" for none). Setting it changes the key and the key text shown in the item; "" removes both. An unknown key is an error.
     .endtable
     
    @@ -598,7 +615,7 @@ These methods work on any control of a form, or on the form itself (Me.AddMenuIt
       CreateMenu         menu$                               Makes an empty popup menu of that name (nothing happens if one exists).
       AddMenu            menu$, caption$                     Adds a menu at the end of the form's menu bar, which appears with the first one (so a form made with CreateForm can have a menu bar). Add items to it by its name. The window keeps its size; the controls get the height the bar takes.
       RemoveMenu         menu$                               Removes a menu of the menu bar with all its items; with the last one the bar goes too. Not for popup menus (DestroyMenu) or items (RemoveMenuItem).
    -  AddMenuItem        menu$, item$, caption$ [, shortcut$]   Adds an item at the end of the menu. Choosing it runs the Sub item_Click, and item.Caption, .Checked, .Enabled, .Visible and .Shortcut work as for .frm items. shortcut$ gives it a key, as Shortcut in the .frm file.
    +  AddMenuItem        menu$, item$, caption$ [, shortcut$]   Adds an item at the end of the menu. Choosing it runs the Sub item_Click, and item.Caption, .Checked, .Enabled, .Visible, and .Shortcut work as for .frm items. shortcut$ gives it a key, as Shortcut in the .frm file.
       AddMenuRadioItem   menu$, item$, caption$ [, checked]  Adds a radio item: radio items next to each other form a group, and choosing one checks it and clears the others.
       AddMenuSeparator   menu$ [, separator$]                Adds a separator line at the end of the menu. With a name it can be hidden and removed like an item.
       AddSubMenu         menu$, submenu$, caption$           Adds a submenu at the end of the menu; add items to it by its name.
    @@ -641,7 +658,7 @@ A level-0 menu that contains children becomes a top-level menu header. A non-lev
     
     .h2 Event Dispatch
     
    -Choosing a menu item (not a header, separator or submenu) fires MenuName_Click, whether it is on the menu bar or in a popup menu.
    +Choosing a menu item (not a header, separator, or submenu) fires MenuName_Click, whether it is on the menu bar or in a popup menu.
     
     .code
     Sub mnuOpen_Click ()
    diff --git a/src/apps/kpunch/dvxbasic/form.dhs b/src/apps/kpunch/dvxbasic/form.dhs
    index a8216285..15439b35 100644
    --- a/src/apps/kpunch/dvxbasic/form.dhs
    +++ b/src/apps/kpunch/dvxbasic/form.dhs
    @@ -52,6 +52,8 @@
     .index Icon (form)
     .index Tag (form)
     .index MousePointer (form)
    +.index FontName (form)
    +.index Font (form)
     .index ActiveControl
     .index FitWidth
     .index FitHeight
    @@ -83,12 +85,13 @@ The Form is the top-level container representing a DVX window. It is declared in
       HelpTopic   String    ""               Help topic opened by F1 while the form is active and the focused control has no HelpTopic of its own. Empty opens the project's help file at its default page.
       Padding     Integer   4                Space in pixels between the window's edges and the controls (the content box's Padding); 0 is none.
       Spacing     Integer   4                Space in pixels between one control and the next (the content box's Spacing); 0 is none.
    -  WindowState Integer   0                vbNormal (0), vbMinimized (1, shown only as an icon at the bottom of the screen) or vbMaximized (2, filling the screen). Set on a hidden form (or in the .frm file), it takes effect when the form is shown; reading it then gives the state the form will have.
    +  WindowState Integer   0                vbNormal (0), vbMinimized (1, shown only as an icon at the bottom of the screen), or vbMaximized (2, filling the screen). Set on a hidden form (or in the .frm file), it takes effect when the form is shown; reading it then gives the state the form will have.
       Controls    Collection  (none)         The form's controls (read-only), in the order they were made: Me.Controls.Count, Me.Controls(1) (the first), Me.Controls("Text1"), For Each c In Me.Controls. Elements of a control array are in it too but have no name key. Each use makes a new Collection; changing it does not change the form.
       DesignMode  Boolean   False            True puts the form in design mode, for a form designer: see Designing Forms below. Not in .frm files.
    -  Icon        Picture   (DVX's)          The picture shown for the window while it is minimized: a picture file name (BMP, PNG, JPEG or GIF) or a picture from LOADPICTURE (Set works too); "" puts DVX's back. Reading it gives a Picture object. Also in the .frm file. A file that is not a picture is an error.
    +  Icon        Picture   (DVX's)          The picture shown for the window while it is minimized: a picture file name (BMP, PNG, JPEG, or GIF) or a picture from LOADPICTURE (Set works too); "" puts DVX's back. Reading it gives a Picture object. Also in the .frm file. A file that is not a picture is an error.
       Tag         String    ""               Any text the program wants to keep with the form, as for controls.
       MousePointer Integer  0                The mouse pointer over the form where its controls have none (see MousePointer under Common Properties).
    +  FontName    String    "System"         The form's font: the font of every control on it that has none of its own, and of the form's Print and TextWidth. FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru go with it, as for controls (see Fonts under Common Properties). Also in the .frm file.
       ActiveControl Control (none)           The control of the form that has the keyboard focus (read-only), Nothing when none of them has it: Me.ActiveControl.SelText for an Edit menu's Copy. Screen.ActiveControl is the focused control of the whole program. Not in .frm files.
       DesignSelection  String  ""            The name of the control (Text1, or Text1(2) in a control array) whose selection handles are drawn while the form is in design mode; "" for none. Not in .frm files.
     .endtable
    @@ -108,14 +111,14 @@ frmTerminal.WindowState = vbMinimized   ' starts as an icon
       Load          (none)                  Fires after the form and all controls are created. This is the default event.
       Unload        (none)                  Fires when the form is being closed or unloaded.
       QueryUnload   Cancel As Integer       Fires before Unload. Set Cancel = 1 to abort the close.
    -  Resize        (none)                  Fires when the window changes size: resized by the user, maximized or restored (by the user or WindowState).
    +  Resize        (none)                  Fires when the window changes size: resized by the user, maximized, or restored (by the user or WindowState).
       Activate      (none)                  Fires when the window gains focus.
       Deactivate    (none)                  Fires when the window loses focus.
       Paint         (none)                  Fires when the form is shown or resized and on Refresh, unless AutoRedraw is True (see Drawing on Forms and PictureBoxes).
       DragDrop      Source As Control, X As Integer, Y As Integer                     Fires when a dragged control is dropped on an empty part of the form. X and Y are where, in the form.
    -  DragOver      Source As Control, X As Integer, Y As Integer, State As Integer   Fires while a dragged control moves over an empty part of the form. State is vbEnter, vbLeave or vbOver.
    +  DragOver      Source As Control, X As Integer, Y As Integer, State As Integer   Fires while a dragged control moves over an empty part of the form. State is vbEnter, vbLeave, or vbOver.
       NewArgs       (none)                  Fires when the running program is handed new arguments: COMMAND$ has changed (see New Arguments below).
    -  DesignMouse   Action As Integer, Button As Integer, X As Integer, Y As Integer, CtrlName As String, Handle As Integer   In design mode: the mouse was pressed, moved, released or double-clicked in the form. See Designing Forms below.
    +  DesignMouse   Action As Integer, Button As Integer, X As Integer, Y As Integer, CtrlName As String, Handle As Integer   In design mode: the mouse was pressed, moved, released, or double-clicked in the form. See Designing Forms below.
       DesignKey     KeyCode As Integer, Shift As Integer   In design mode: a key was pressed in the form (KeyCode and Shift as in KeyDown).
       DesignPropertyChanged   CtrlName As String, PropertyName As String   In design mode: the code of a UserControl on the form called PropertyChanged. See Designing Forms below.
     .endtable
    @@ -126,7 +129,7 @@ See Drag and Drop under Common Properties, Events, and Methods. While a control
     
     A program with a NewArgs event procedure on any of its forms can be handed new arguments while it runs, instead of DVX starting another copy of it. COMMAND$ (and COMMAND$(1), COMMAND$(2), ...) then give the new arguments, and NewArgs fires on every loaded form that has the procedure. When the arguments arrive while one of the program's event procedures is still running -- for example while it shows a message box or a dialog, or a form with vbModal -- they are held, and NewArgs fires once that procedure has finished; if several arrive meanwhile, only the last is given.
     
    -DVX hands new arguments to the help viewer: F1 and the Help menus of programs (and HelpView in BASIC) send "helpfile topic" to the program installed as APPS\KPUNCH\DVXHELP\DVXHELP.APP when it is already running. A help viewer written in BASIC and installed there gets them in NewArgs. Starting a program that is already running -- from Program Manager, the Run dialog, a document opened in it or ShellRun -- gives it the new arguments when it allows only one copy and has a NewArgs procedure: its window comes to the front and NewArgs fires. Without a NewArgs procedure DVX says it is already running; a program that allows more than one copy (Project Properties) starts another copy instead.
    +DVX hands new arguments to the help viewer: F1 and the Help menus of programs (and HelpView in BASIC) send "helpfile topic" to the program installed as APPS\KPUNCH\DVXHELP\DVXHELP.APP when it is already running. A help viewer written in BASIC and installed there gets them in NewArgs. Starting a program that is already running -- from Program Manager, the Run dialog, a document opened in it, or ShellRun -- gives it the new arguments when it allows only one copy and has a NewArgs procedure: its window comes to the front and NewArgs fires. Without a NewArgs procedure DVX says it is already running; a program that allows more than one copy (Project Properties) starts another copy instead.
     
     .code
     Sub Form1_NewArgs ()
    @@ -153,7 +156,7 @@ End Sub
     
     .h2 Several Copies of a Form
     
    -A form can be open more than once, the way a program opens a second document window. New FormName makes another copy of the form: its own window, controls, menus and form-level variables, built from the same .frm. Its Load event runs, and it starts hidden like any loaded form, so call Show. Keep the copy in a variable declared As the form's name (or As Form, As Object):
    +A form can be open more than once, the way a program opens a second document window. New FormName makes another copy of the form: its own window, controls, menus, and form-level variables, built from the same .frm. Its Load event runs, and it starts hidden like any loaded form, so call Show. Keep the copy in a variable declared As the form's name (or As Form, As Object):
     
     .code
     Dim f As Form1          ' a Form1 copy, or the form itself
    @@ -165,7 +168,7 @@ f.Show
     Dim g As New Form1      ' declares and makes a copy at once
     .endcode
     
    -The code of the form serves every copy. In it, Me, the controls named on their own (Text1) and the form-level variables are those of the copy whose event is running, so each window keeps its own state. Unload f closes that copy only.
    +The code of the form serves every copy. In it, Me, the controls named on their own (Text1), and the form-level variables are those of the copy whose event is running, so each window keeps its own state. Unload f closes that copy only.
     
     The form's name (Form1.Caption, Form1.Show, Load Form1) always means the first copy loaded -- the one the program starts with -- as in Visual Basic. If that copy was unloaded, the name means the next one still open.
     
    @@ -174,12 +177,12 @@ The form's name (Form1.Caption, Form1.Show, Load Form1) always means the first c
     A form designer written in BASIC shows the form being designed as a real form and turns on its design mode. While DesignMode is True, the controls on the form take no mouse clicks or keys and fire no events (Timers too are quiet); the form instead tells the designer what the user does:
     
     .list
    -.item DesignMouse (Action, Button, X, Y, CtrlName, Handle): Action is DSGN_MOUSE_DOWN (1, a button was pressed), DSGN_MOUSE_MOVE (2), DSGN_MOUSE_UP (3) or DSGN_MOUSE_DBLCLICK (4, a double click: open the code, say). Button is the button pressed or released (1 left, 2 right, 4 middle; for a move, the buttons held). X and Y are where, in the form. CtrlName is the control there -- the innermost one when containers hold others -- or "" for the form itself. Handle is the selection handle of the selected control under the mouse: DSGN_HANDLE_E (3, the right edge: drag to change the width), DSGN_HANDLE_SE (4, the corner) or DSGN_HANDLE_S (5, the bottom edge), or DSGN_HANDLE_NONE (-1).
    -.item DesignKey (KeyCode, Shift): a key, for Delete, the arrow keys and so on.
    +.item DesignMouse (Action, Button, X, Y, CtrlName, Handle): Action is DSGN_MOUSE_DOWN (1, a button was pressed), DSGN_MOUSE_MOVE (2), DSGN_MOUSE_UP (3), or DSGN_MOUSE_DBLCLICK (4, a double click: open the code, say). Button is the button pressed or released (1 left, 2 right, 4 middle; for a move, the buttons held). X and Y are where, in the form. CtrlName is the control there -- the innermost one when containers hold others -- or "" for the form itself. Handle is the selection handle of the selected control under the mouse: DSGN_HANDLE_E (3, the right edge: drag to change the width), DSGN_HANDLE_SE (4, the corner), or DSGN_HANDLE_S (5, the bottom edge), or DSGN_HANDLE_NONE (-1).
    +.item DesignKey (KeyCode, Shift): a key, for Delete, the arrow keys, and so on.
     .item DesignPropertyChanged (CtrlName, PropertyName): the code of a UserControl on the form -- one of the project's own, put on the designer's toolbox with CompDesignControls (compiler.bas) -- called UserControl.PropertyChanged PropertyName: read its properties again. Such controls run their code on the form with UserControl.UserMode False.
     .endlist
     
    -DesignSelection names the selected control; the form draws its eight selection handles over the controls (the three that resize in black) and shows the resize mouse pointers over them. The constants are in designer.bas, which also lists the control types, their properties and events (see Include Libraries in the language reference). Together with CreateControl, RemoveControl, CallByName, Controls, Container, MoveBefore, LoadFormText and the .frm model of designer.bas (FrmOpen, FrmText$, FrmCtlText$ / FrmCtlPaste to copy and paste controls, FormText$ to write the live form as .frm text), a designer has what the DVX BASIC IDE's own designer uses.
    +DesignSelection names the selected control; the form draws its eight selection handles over the controls (the three that resize in black) and shows the resize mouse pointers over them. The constants are in designer.bas, which also lists the control types, their properties, and events (see Include Libraries in the language reference). Together with CreateControl, RemoveControl, CallByName, Controls, Container, MoveBefore, LoadFormText, and the .frm model of designer.bas (FrmOpen, FrmText$, FrmCtlText$ / FrmCtlPaste to copy and paste controls, FormText$ to write the live form as .frm text), a designer has what the DVX BASIC IDE's own designer uses.
     
     .code
     Sub Canvas_DesignMouse(Action As Integer, Button As Integer, X As Integer, Y As Integer, CtrlName As String, Handle As Integer)
    @@ -198,7 +201,7 @@ End Sub
     
     .h2 Drawing
     
    -Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, ScrollBarsNeeded, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, FontScale, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle and so on.
    +Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, ScrollBarsNeeded, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, FontScale, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle, and so on.
     
     .h2 Example
     
    diff --git a/src/apps/kpunch/dvxbasic/formrt/formGfx.c b/src/apps/kpunch/dvxbasic/formrt/formGfx.c
    index 45c81136..1ea69746 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/formGfx.c
    +++ b/src/apps/kpunch/dvxbasic/formrt/formGfx.c
    @@ -113,7 +113,7 @@
     #define DRAW_SB_CLICK_CTRL_ARG  6       // ... , control
     #define DRAW_SB_LINE_ROWS       1       // the rows a control bar's arrow moves
     #define DRAW_SB_NEEDS_ARGS      4       // ScrollBarsNeeded(contentWidth, contentHeight, areaWidth, areaHeight)
    -#define DRAW_FONT_SCALE_NORMAL  1       // FontScale: the 8x16 font as it is
    +#define DRAW_FONT_SCALE_NORMAL  1       // FontScale: the font as it is
     #define DRAW_FONT_SCALE_MAX     8       // FontScale: the largest multiple
     #define DRAW_FACE_NONE          (-1)    // DrawBevel face: leave the inside as it is
     #define DRAW_ACCEL_ENABLED_ARG  1       // PrintAccel text [, enabled]
    @@ -175,7 +175,7 @@ struct BasDrawT {
         WidgetT     *canvas;         // the PictureBox, or the form's drawing area (NULL until a form draws)
         AppContextT *ctx;
         int32_t      scaleMode;      // VB ScaleMode (BAS_SCALE_*)
    -    int32_t      fontScale;      // FontScale: Print's text this many times as large
    +    int32_t      fontScale;      // FontScale: Print's text this many times as large (in the object's font, drawFont)
         bool         autoRedraw;     // True: no Paint events
         bool         autoRedrawSet;  // the program set AutoRedraw (False then clears to BackColor before Paint)
         bool         cacheValid;     // colour -> pixel cache
    @@ -281,13 +281,14 @@ static int32_t   drawCls(BasDrawT *d);
     static int32_t   drawFind(const char *const *names, int32_t count, const char *name);
     static void      drawFocusDot(void *ctx, int32_t x, int32_t y);
     static int32_t   drawFocusRectOn(BasDrawT *d, const BasValueT *args, int32_t argc);
    +static const BitmapFontT *drawFont(const BasControlT *ctrl);
     static BasDrawT *drawFor(BasFormRtT *rt, BasControlT *ctrl, bool needCanvas);
     static bool      drawIsSurface(const BasControlT *ctrl);
    -static bool      drawMeasure(BasFormRtT *rt, BasDrawT *d, DrawMethodE method, const BasValueT *args, int32_t argc, BasValueT *result);
    +static bool      drawMeasure(BasDrawT *d, const BitmapFontT *font, DrawMethodE method, const BasValueT *args, int32_t argc, BasValueT *result);
     static void      drawPaintKeyed(BasFormRtT *rt, BasDrawT *d, WidgetT *src, const uint8_t *pixels, int32_t pitch, uint32_t opcode, const BasValueT *args, int32_t argc, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy);
     static void      drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, BasValueT *args, int32_t argc);
     static int32_t   drawPixelSpan(const BasGfxT *g, double length, bool across);
    -static void      drawPrint(BasDrawT *d, BasValueT value, int32_t mode);
    +static void      drawPrint(BasDrawT *d, const BitmapFontT *font, BasValueT value, int32_t mode);
     static uint32_t  drawRasterOp(uint32_t rop, uint32_t pattern, uint32_t source, uint32_t dest);
     static int32_t   drawScale(BasDrawT *d, const BasValueT *args);
     static void      drawScaleApply(BasDrawT *d);
    @@ -295,7 +296,7 @@ static int32_t   drawScaleArg(const BasValueT *args, int32_t argc, int32_t idx,
     static bool      drawScaleLength(BasDrawT *d, const BasValueT *args, int32_t argc, bool across, double *out);
     static bool      drawSetScaleProp(BasFormRtT *rt, BasDrawT *d, int32_t prop, BasValueT value);
     static void      drawSync(BasDrawT *d);
    -static int32_t   drawThemed(BasFormRtT *rt, BasDrawT *d, DrawMethodE method, const BasValueT *args, int32_t argc);
    +static int32_t   drawThemed(BasFormRtT *rt, BasDrawT *d, const BitmapFontT *font, DrawMethodE method, const BasValueT *args, int32_t argc);
     static double    drawToPixelLength(BasDrawT *d, double length, int32_t scale, bool across);
     static void      drawToPixels(const BasGfxT *g, double x, double y, double w, double h, int32_t *px, int32_t *py, int32_t *pw, int32_t *ph);
     static uint32_t  readPixel(const uint8_t *src, int32_t bytesPerPixel);
    @@ -425,13 +426,14 @@ bool basFormRtDrawMethod(BasFormRtT *rt, BasControlT *ctrl, const char *name, Ba
         *result = basValInteger(0);
     
         // TextWidth / TextHeight need no drawing area: the text's size in the
    -    // object's scale
    +    // object's font and scale
         if (method == DRAW_M_TEXTWIDTH || method == DRAW_M_TEXTHEIGHT) {
    -        BasStringT *s     = argc >= 1 ? basValFormatString(args[0]) : basStringNew("", 0);
    -        BasDrawT   *sized = drawFor(rt, ctrl, false);
    -        bool        wide  = method == DRAW_M_TEXTWIDTH;
    -        int32_t     scale = sized ? sized->fontScale : DRAW_FONT_SCALE_NORMAL;
    -        double      px    = wide ? (double)s->len * rt->ctx->font.charWidth * scale : (double)rt->ctx->font.charHeight * scale;
    +        const BitmapFontT *font  = drawFont(ctrl);
    +        BasStringT        *s     = argc >= 1 ? basValFormatString(args[0]) : basStringNew("", 0);
    +        BasDrawT          *sized = drawFor(rt, ctrl, false);
    +        bool               wide  = method == DRAW_M_TEXTWIDTH;
    +        int32_t            scale = sized ? sized->fontScale : DRAW_FONT_SCALE_NORMAL;
    +        double             px    = wide ? (double)textWidthN(font, s->data, s->len) * scale : (double)font->charHeight * scale;
     
             if (sized) {
                 drawSync(sized);
    @@ -445,8 +447,8 @@ bool basFormRtDrawMethod(BasFormRtT *rt, BasControlT *ctrl, const char *name, Ba
     
         // AccelKey / TextWidthAccel / ScrollBarClick / ScrollBarsNeeded only measure
         if (method == DRAW_M_ACCELKEY || method == DRAW_M_TEXTWIDTHACCEL || method == DRAW_M_SCROLLBARCLICK || method == DRAW_M_SCROLLBARSNEEDED) {
    -        if (!drawMeasure(rt, drawFor(rt, ctrl, false), (DrawMethodE)method, args, argc, result)) {
    -            basFormRtRuntimeError(rt, "Illegal function call", "Method: %s\nAccelKey and TextWidthAccel take a caption; ScrollBarClick takes the bar's length, the click's offset from its top (or left end), the position, the rows shown, the rows in all and optionally True for a horizontal bar; ScrollBarsNeeded takes the content's width and height and the area's width and height.", name);
    +        if (!drawMeasure(drawFor(rt, ctrl, false), drawFont(ctrl), (DrawMethodE)method, args, argc, result)) {
    +            basFormRtRuntimeError(rt, "Illegal function call", "Method: %s\nAccelKey and TextWidthAccel take a caption; ScrollBarClick takes the bar's length, the click's offset from its top (or left end), the position, the rows shown, the rows in all, and optionally True for a horizontal bar; ScrollBarsNeeded takes the content's width and height and the area's width and height.", name);
             }
     
             return true;
    @@ -500,7 +502,7 @@ bool basFormRtDrawMethod(BasFormRtT *rt, BasControlT *ctrl, const char *name, Ba
     
             case DRAW_M_PRINT:
                 if (argc == BAS_OBJ_PRINT_ARGS) {
    -                drawPrint(d, args[0], basValToInt32(args[1]));
    +                drawPrint(d, drawFont(ctrl), args[0], basValToInt32(args[1]));
                 } else {
                     err = BAS_ERR_ILLEGAL_FUNC_CALL;
                 }
    @@ -521,7 +523,7 @@ bool basFormRtDrawMethod(BasFormRtT *rt, BasControlT *ctrl, const char *name, Ba
             case DRAW_M_DRAWSCROLLARROW:
             case DRAW_M_DRAWSCROLLBAR:
             case DRAW_M_PRINTACCEL:
    -            err = drawThemed(rt, d, (DrawMethodE)method, args, argc);
    +            err = drawThemed(rt, d, drawFont(ctrl), (DrawMethodE)method, args, argc);
                 break;
     
             case DRAW_M_SCALE:
    @@ -533,7 +535,7 @@ bool basFormRtDrawMethod(BasFormRtT *rt, BasControlT *ctrl, const char *name, Ba
         }
     
         if (err != 0) {
    -        basFormRtRuntimeError(rt, "Illegal function call", "Method: %s\nAn argument is out of range (a colour, a radius, an angle or an empty Scale).", name);
    +        basFormRtRuntimeError(rt, "Illegal function call", "Method: %s\nAn argument is out of range (a colour, a radius, an angle, or an empty Scale).", name);
         }
     
         return true;
    @@ -784,6 +786,16 @@ static int32_t drawFocusRectOn(BasDrawT *d, const BasValueT *args, int32_t argc)
     }
     
     
    +// The font the drawing members of ctrl write and measure text in: the
    +// one its drawing area draws with (wgtFont), or before a form has one,
    +// the form's, which the area takes.
    +static const BitmapFontT *drawFont(const BasControlT *ctrl) {
    +    const BasDrawT *d = ctrl->draw;
    +
    +    return wgtFont((d && d->canvas) ? d->canvas : ctrl->widget);
    +}
    +
    +
     // The control's drawing state, made on first use; with needCanvas a form
     // also gets its drawing area (a canvas the size of its content, added to
     // its controls).
    @@ -854,8 +866,9 @@ static bool drawIsSurface(const BasControlT *ctrl) {
     // ScrollBarsNeeded(contentWidth, contentHeight, areaWidth, areaHeight) --
     // the bars content that big needs in an area that big, as a control's
     // (widgetScrollbarNeeds: each bar can make the other one needed), as
    -// vbSBNone, vbHorizontal, vbVertical or vbBoth.  False for bad arguments.
    -static bool drawMeasure(BasFormRtT *rt, BasDrawT *d, DrawMethodE method, const BasValueT *args, int32_t argc, BasValueT *result) {
    +// vbSBNone, vbHorizontal, vbVertical or vbBoth.  Text is measured in
    +// font.  False for bad arguments.
    +static bool drawMeasure(BasDrawT *d, const BitmapFontT *font, DrawMethodE method, const BasValueT *args, int32_t argc, BasValueT *result) {
         if (method == DRAW_M_SCROLLBARSNEEDED) {
             if (argc != DRAW_SB_NEEDS_ARGS) {
                 return false;
    @@ -915,7 +928,7 @@ static bool drawMeasure(BasFormRtT *rt, BasDrawT *d, DrawMethodE method, const B
             result->type   = BAS_TYPE_STRING;
             result->strVal = basStringNew(&key, key ? 1 : 0);
         } else {
    -        double px = (double)textWidthAccel(&rt->ctx->font, text->data);
    +        double px = (double)textWidthAccel(font, text->data);
     
             if (d) {
                 drawSync(d);
    @@ -979,7 +992,7 @@ static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, Bas
         int32_t   bpp    = rt->ctx->display.format.bytesPerPixel;
     
         if (argc < DRAW_PAINT_MIN_ARGS) {
    -        basFormRtRuntimeError(rt, "PaintPicture needs a picture, x and y", "Usage: PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight [, opcode]]]");
    +        basFormRtRuntimeError(rt, "PaintPicture needs a picture, x, and y", "Usage: PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight [, opcode]]]");
             return;
         }
     
    @@ -1001,7 +1014,7 @@ static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, Bas
         }
     
         if (!pixels && (!src || !canvasSize(src, &srcW, &srcH))) {
    -        basFormRtRuntimeError(rt, "PaintPicture has no picture", "The picture must be a PictureBox (its name, or a Control variable holding it), a BMP, PNG, JPEG or GIF image file, a program, widget or library file (its icon), or a picture from LoadPicture.");
    +        basFormRtRuntimeError(rt, "PaintPicture has no picture", "The picture must be a PictureBox (its name, or a Control variable holding it), a BMP, PNG, JPEG, or GIF image file, a program, widget or library file (its icon), or a picture from LoadPicture.");
             return;
         }
     
    @@ -1087,14 +1100,15 @@ static int32_t drawPixelSpan(const BasGfxT *g, double length, bool across) {
     }
     
     
    -// One object Print item at CurrentX / CurrentY in ForeColor (the DVX
    -// font FontScale times as large, transparent background).  Columns,
    -// zones and lines are counted in (scaled) characters from the left edge,
    -// CurrentX / CurrentY kept in the scale.
    -static void drawPrint(BasDrawT *d, BasValueT value, int32_t mode) {
    +// One object Print item at CurrentX / CurrentY in ForeColor (font
    +// FontScale times as large, transparent background).  Columns, zones and
    +// lines are counted in (scaled) characters of the font -- its charWidth
    +// across, its line height down -- from the left edge, CurrentX /
    +// CurrentY kept in the scale; text moves CurrentX by its width.
    +static void drawPrint(BasDrawT *d, const BitmapFontT *font, BasValueT value, int32_t mode) {
         BasGfxT *g      = &d->gfx;
    -    int32_t  charW  = d->ctx->font.charWidth * d->fontScale;
    -    int32_t  charH  = d->ctx->font.charHeight * d->fontScale;
    +    int32_t  charW  = font->charWidth * d->fontScale;
    +    int32_t  charH  = font->charHeight * d->fontScale;
         double   left   = g->world ? g->worldLeft : 0.0;
         double   px     = basGfxToPixelX(g, g->lastX);
         int32_t  column = (int32_t)(px / charW);
    @@ -1133,7 +1147,7 @@ static void drawPrint(BasDrawT *d, BasValueT value, int32_t mode) {
     
         wgtCanvasSetPenColor(d->canvas, basGfxPixel(g, g->fore));
         wgtCanvasDrawTextScaled(d->canvas, basGfxRound(px), basGfxRound(basGfxToPixelY(g, g->lastY)), s->data, d->fontScale);
    -    g->lastX += basGfxToUnitsX(g, (double)s->len * charW);
    +    g->lastX += basGfxToUnitsX(g, (double)textWidthN(font, s->data, s->len) * d->fontScale);
         basStringUnref(s);
     }
     
    @@ -1327,9 +1341,9 @@ static void drawSync(BasDrawT *d) {
     //   DrawScrollArrow x, y, size, direction               -- drawScrollbarArrowButton
     //   DrawScrollBar x, y, length, pos, visible, total [, horizontal [, control]]  -- wmDrawScrollbarAt (control: widgetDrawScrollbarV / H)
     //   PrintAccel text [, enabled]                         -- drawWidgetTextAccel
    -// (the face defaults to the theme's, the arrow and text to ForeColor).
    -// 0 or a BASIC error.
    -static int32_t drawThemed(BasFormRtT *rt, BasDrawT *d, DrawMethodE method, const BasValueT *args, int32_t argc) {
    +// (the face defaults to the theme's, the arrow and text to ForeColor;
    +// the text in font).  0 or a BASIC error.
    +static int32_t drawThemed(BasFormRtT *rt, BasDrawT *d, const BitmapFontT *font, DrawMethodE method, const BasValueT *args, int32_t argc) {
         const ColorSchemeT *cs  = &rt->ctx->colors;
         const BlitOpsT     *ops = &rt->ctx->blitOps;
         BasGfxT            *g   = &d->gfx;
    @@ -1351,8 +1365,8 @@ static int32_t drawThemed(BasFormRtT *rt, BasDrawT *d, DrawMethodE method, const
             BasStringT *text    = basValFormatString(args[0]);
             bool        enabled = argc <= DRAW_ACCEL_ENABLED_ARG || basValIsTruthy(args[DRAW_ACCEL_ENABLED_ARG]);
     
    -        drawWidgetTextAccel(&disp, ops, &rt->ctx->font, basGfxRound(basGfxToPixelX(g, g->lastX)), basGfxRound(basGfxToPixelY(g, g->lastY)), text->data, surfToPixel(d, g->fore), 0, false, enabled, cs);
    -        g->lastX += basGfxToUnitsX(g, (double)textWidthAccel(&rt->ctx->font, text->data));
    +        drawWidgetTextAccel(&disp, ops, font, basGfxRound(basGfxToPixelX(g, g->lastX)), basGfxRound(basGfxToPixelY(g, g->lastY)), text->data, surfToPixel(d, g->fore), 0, false, enabled, cs);
    +        g->lastX += basGfxToUnitsX(g, (double)textWidthAccel(font, text->data));
             basStringUnref(text);
             wgtInvalidatePaint(d->canvas);
             return 0;
    diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.c b/src/apps/kpunch/dvxbasic/formrt/formrt.c
    index 856ee231..ada96320 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/formrt.c
    +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.c
    @@ -41,6 +41,7 @@
     #include "dvxDlg.h"
     #include "dvxPlat.h"
     #include "dvxRes.h"
    +#include "dvxText.h"
     #include "dvxWgtP.h"
     #include "dvxWm.h"
     #include "shellApp.h"
    @@ -61,6 +62,7 @@
     
     #include 
     #include 
    +#include 
     #include 
     #include 
     #include 
    @@ -98,7 +100,7 @@ DVX_ABI_STAMP(basrt);
     #define BAS_MANGLED_NAME_LEN (BAS_MAX_IDENT + 2)
     
     // Comma-separated property-name list for "Valid: ..." diagnostics.
    -#define BAS_PROP_LIST_LEN    256
    +#define BAS_PROP_LIST_LEN    384
     
     // Largest pixel size a control size property accepts; values are
     // clamped to [0, BAS_CTRL_MAX_DIM] before they become size hints.
    @@ -185,6 +187,11 @@ static bool sNewFormCopy = false;
     // default number.
     static bool sLastInputBoxCancelled = false;
     
    +// The font the last DlgFont call ended on, which DlgFontName$ /
    +// DlgFontSize / DlgFontStyle give: the one chosen, or after Cancel the
    +// one it started at.
    +static DvxFontChoiceT sDlgFont;
    +
     // The Clipboard object: basFormRtFindCtrl hands out this address for the
     // name Clipboard when no control or form has it; only its address is used.
     static const uint8_t sClipboardObj = 0;
    @@ -460,7 +467,10 @@ typedef struct {
     
     // ctrlStack/menuIdxStack parallel the parser's block stack so a
     // parent's properties that follow a nested child's End still reach the
    -// parent.
    +// parent.  A block's Font properties are collected into font and given
    +// to fontOwner (the control's widget, the form's root) in one go when
    +// another block starts or ends (frmLoadFontFlush), so their order in
    +// the file does not matter.
     typedef struct {
         BasFormRtT         *rt;
         BasFormT           *form;
    @@ -475,6 +485,8 @@ typedef struct {
         int32_t             curMenuItemIdx;
         int32_t             menuIdxStack[FRM_MAX_NESTING];
         int32_t             menuIdxDepth;
    +    WidgetT            *fontOwner;                         // whose Font properties font holds (NULL: none)
    +    DvxFontChoiceT      font;
     } BasFrmLoadCtxT;
     
     
    @@ -507,6 +519,12 @@ typedef enum {
         FORM_PROP_ICON,
         FORM_PROP_TAG,
         FORM_PROP_MOUSEPOINTER,
    +    FORM_PROP_FONTNAME,
    +    FORM_PROP_FONTSIZE,
    +    FORM_PROP_FONTBOLD,
    +    FORM_PROP_FONTITALIC,
    +    FORM_PROP_FONTUNDERLINE,
    +    FORM_PROP_FONTSTRIKETHRU,
         FORM_PROP_ACTIVECONTROL,
         FORM_PROP_COUNT
     } BasFormPropE;
    @@ -534,6 +552,12 @@ static const BasPropDescT sFormProps[FORM_PROP_COUNT] = {
         [FORM_PROP_ICON]            = { BAS_PROP_ICON,            WGT_IFACE_STRING, true, true  },
         [FORM_PROP_TAG]             = { BAS_PROP_TAG,             WGT_IFACE_STRING, true, true  },
         [FORM_PROP_MOUSEPOINTER]    = { BAS_PROP_MOUSEPOINTER,    WGT_IFACE_INT,    true, true  },
    +    [FORM_PROP_FONTNAME]        = { BAS_PROP_FONTNAME,        WGT_IFACE_STRING, true, true  },
    +    [FORM_PROP_FONTSIZE]        = { BAS_PROP_FONTSIZE,        WGT_IFACE_INT,    true, true  },
    +    [FORM_PROP_FONTBOLD]        = { BAS_PROP_FONTBOLD,        WGT_IFACE_BOOL,   true, true  },
    +    [FORM_PROP_FONTITALIC]      = { BAS_PROP_FONTITALIC,      WGT_IFACE_BOOL,   true, true  },
    +    [FORM_PROP_FONTUNDERLINE]   = { BAS_PROP_FONTUNDERLINE,   WGT_IFACE_BOOL,   true, true  },
    +    [FORM_PROP_FONTSTRIKETHRU]  = { BAS_PROP_FONTSTRIKETHRU,  WGT_IFACE_BOOL,   true, true  },
         [FORM_PROP_ACTIVECONTROL]   = { BAS_PROP_ACTIVECONTROL,   BAS_PROP_OBJECT,  true, false },
     };
     
    @@ -556,6 +580,12 @@ typedef enum {
         CTRL_PROP_READONLY,
         CTRL_PROP_BACKCOLOR,
         CTRL_PROP_FORECOLOR,
    +    CTRL_PROP_FONTNAME,
    +    CTRL_PROP_FONTSIZE,
    +    CTRL_PROP_FONTBOLD,
    +    CTRL_PROP_FONTITALIC,
    +    CTRL_PROP_FONTUNDERLINE,
    +    CTRL_PROP_FONTSTRIKETHRU,
         CTRL_PROP_CAPTION,
         CTRL_PROP_TEXT,
         CTRL_PROP_TOOLTIPTEXT,
    @@ -574,35 +604,67 @@ typedef enum {
     } BasCtrlPropE;
     
     static const BasPropDescT sCtrlProps[CTRL_PROP_COUNT] = {
    -    [CTRL_PROP_NAME]         = { BAS_PROP_NAME,         WGT_IFACE_STRING, true, false },
    -    [CTRL_PROP_LEFT]         = { BAS_PROP_LEFT,         WGT_IFACE_INT,    true, true  },
    -    [CTRL_PROP_TOP]          = { BAS_PROP_TOP,          WGT_IFACE_INT,    true, true  },
    -    [CTRL_PROP_WIDTH]        = { BAS_PROP_WIDTH,        WGT_IFACE_INT,    true, true  },
    -    [CTRL_PROP_HEIGHT]       = { BAS_PROP_HEIGHT,       WGT_IFACE_INT,    true, true  },
    -    [CTRL_PROP_MINWIDTH]     = { BAS_PROP_MINWIDTH,     WGT_IFACE_INT,    true, true  },
    -    [CTRL_PROP_MINHEIGHT]    = { BAS_PROP_MINHEIGHT,    WGT_IFACE_INT,    true, true  },
    -    [CTRL_PROP_MAXWIDTH]     = { BAS_PROP_MAXWIDTH,     WGT_IFACE_INT,    true, true  },
    -    [CTRL_PROP_MAXHEIGHT]    = { BAS_PROP_MAXHEIGHT,    WGT_IFACE_INT,    true, true  },
    -    [CTRL_PROP_WEIGHT]       = { BAS_PROP_WEIGHT,       WGT_IFACE_INT,    true, true  },
    -    [CTRL_PROP_VISIBLE]      = { BAS_PROP_VISIBLE,      WGT_IFACE_BOOL,   true, true  },
    -    [CTRL_PROP_ENABLED]      = { BAS_PROP_ENABLED,      WGT_IFACE_BOOL,   true, true  },
    -    [CTRL_PROP_READONLY]     = { BAS_PROP_READONLY,     WGT_IFACE_BOOL,   true, true  },
    -    [CTRL_PROP_BACKCOLOR]    = { BAS_PROP_BACKCOLOR,    WGT_IFACE_INT,    true, true  },
    -    [CTRL_PROP_FORECOLOR]    = { BAS_PROP_FORECOLOR,    WGT_IFACE_INT,    true, true  },
    -    [CTRL_PROP_CAPTION]      = { BAS_PROP_CAPTION,      WGT_IFACE_STRING, true, true  },
    -    [CTRL_PROP_TEXT]         = { BAS_PROP_TEXT,         WGT_IFACE_STRING, true, true  },
    -    [CTRL_PROP_TOOLTIPTEXT]  = { BAS_PROP_TOOLTIPTEXT,  WGT_IFACE_STRING, true, true  },
    -    [CTRL_PROP_CONTEXTMENU]  = { BAS_PROP_CONTEXTMENU,  WGT_IFACE_STRING, true, true  },
    -    [CTRL_PROP_HELPTOPIC]    = { BAS_PROP_HELPTOPIC,    WGT_IFACE_STRING, true, true  },
    -    [CTRL_PROP_DATASOURCE]   = { BAS_PROP_DATASOURCE,   WGT_IFACE_STRING, true, true  },
    -    [CTRL_PROP_DATAFIELD]    = { BAS_PROP_DATAFIELD,    WGT_IFACE_STRING, true, true  },
    -    [CTRL_PROP_LISTCOUNT]    = { BAS_PROP_LISTCOUNT,    WGT_IFACE_INT,    true, false },
    -    [CTRL_PROP_DRAGMODE]     = { BAS_PROP_DRAGMODE,     WGT_IFACE_INT,    true, true  },
    -    [CTRL_PROP_DRAGICON]     = { BAS_PROP_DRAGICON,     WGT_IFACE_STRING, true, true  },
    -    [CTRL_PROP_PARENT]       = { BAS_PROP_PARENT,       BAS_PROP_OBJECT,  true, false },
    -    [CTRL_PROP_CONTAINER]    = { BAS_PROP_CONTAINER,    BAS_PROP_OBJECT,  true, true  },
    -    [CTRL_PROP_TAG]          = { BAS_PROP_TAG,          WGT_IFACE_STRING, true, true  },
    -    [CTRL_PROP_MOUSEPOINTER] = { BAS_PROP_MOUSEPOINTER, WGT_IFACE_INT,    true, true  },
    +    [CTRL_PROP_NAME]           = { BAS_PROP_NAME,           WGT_IFACE_STRING, true, false },
    +    [CTRL_PROP_LEFT]           = { BAS_PROP_LEFT,           WGT_IFACE_INT,    true, true  },
    +    [CTRL_PROP_TOP]            = { BAS_PROP_TOP,            WGT_IFACE_INT,    true, true  },
    +    [CTRL_PROP_WIDTH]          = { BAS_PROP_WIDTH,          WGT_IFACE_INT,    true, true  },
    +    [CTRL_PROP_HEIGHT]         = { BAS_PROP_HEIGHT,         WGT_IFACE_INT,    true, true  },
    +    [CTRL_PROP_MINWIDTH]       = { BAS_PROP_MINWIDTH,       WGT_IFACE_INT,    true, true  },
    +    [CTRL_PROP_MINHEIGHT]      = { BAS_PROP_MINHEIGHT,      WGT_IFACE_INT,    true, true  },
    +    [CTRL_PROP_MAXWIDTH]       = { BAS_PROP_MAXWIDTH,       WGT_IFACE_INT,    true, true  },
    +    [CTRL_PROP_MAXHEIGHT]      = { BAS_PROP_MAXHEIGHT,      WGT_IFACE_INT,    true, true  },
    +    [CTRL_PROP_WEIGHT]         = { BAS_PROP_WEIGHT,         WGT_IFACE_INT,    true, true  },
    +    [CTRL_PROP_VISIBLE]        = { BAS_PROP_VISIBLE,        WGT_IFACE_BOOL,   true, true  },
    +    [CTRL_PROP_ENABLED]        = { BAS_PROP_ENABLED,        WGT_IFACE_BOOL,   true, true  },
    +    [CTRL_PROP_READONLY]       = { BAS_PROP_READONLY,       WGT_IFACE_BOOL,   true, true  },
    +    [CTRL_PROP_BACKCOLOR]      = { BAS_PROP_BACKCOLOR,      WGT_IFACE_INT,    true, true  },
    +    [CTRL_PROP_FORECOLOR]      = { BAS_PROP_FORECOLOR,      WGT_IFACE_INT,    true, true  },
    +    [CTRL_PROP_FONTNAME]       = { BAS_PROP_FONTNAME,       WGT_IFACE_STRING, true, true  },
    +    [CTRL_PROP_FONTSIZE]       = { BAS_PROP_FONTSIZE,       WGT_IFACE_INT,    true, true  },
    +    [CTRL_PROP_FONTBOLD]       = { BAS_PROP_FONTBOLD,       WGT_IFACE_BOOL,   true, true  },
    +    [CTRL_PROP_FONTITALIC]     = { BAS_PROP_FONTITALIC,     WGT_IFACE_BOOL,   true, true  },
    +    [CTRL_PROP_FONTUNDERLINE]  = { BAS_PROP_FONTUNDERLINE,  WGT_IFACE_BOOL,   true, true  },
    +    [CTRL_PROP_FONTSTRIKETHRU] = { BAS_PROP_FONTSTRIKETHRU, WGT_IFACE_BOOL,   true, true  },
    +    [CTRL_PROP_CAPTION]        = { BAS_PROP_CAPTION,        WGT_IFACE_STRING, true, true  },
    +    [CTRL_PROP_TEXT]           = { BAS_PROP_TEXT,           WGT_IFACE_STRING, true, true  },
    +    [CTRL_PROP_TOOLTIPTEXT]    = { BAS_PROP_TOOLTIPTEXT,    WGT_IFACE_STRING, true, true  },
    +    [CTRL_PROP_CONTEXTMENU]    = { BAS_PROP_CONTEXTMENU,    WGT_IFACE_STRING, true, true  },
    +    [CTRL_PROP_HELPTOPIC]      = { BAS_PROP_HELPTOPIC,      WGT_IFACE_STRING, true, true  },
    +    [CTRL_PROP_DATASOURCE]     = { BAS_PROP_DATASOURCE,     WGT_IFACE_STRING, true, true  },
    +    [CTRL_PROP_DATAFIELD]      = { BAS_PROP_DATAFIELD,      WGT_IFACE_STRING, true, true  },
    +    [CTRL_PROP_LISTCOUNT]      = { BAS_PROP_LISTCOUNT,      WGT_IFACE_INT,    true, false },
    +    [CTRL_PROP_DRAGMODE]       = { BAS_PROP_DRAGMODE,       WGT_IFACE_INT,    true, true  },
    +    [CTRL_PROP_DRAGICON]       = { BAS_PROP_DRAGICON,       WGT_IFACE_STRING, true, true  },
    +    [CTRL_PROP_PARENT]         = { BAS_PROP_PARENT,         BAS_PROP_OBJECT,  true, false },
    +    [CTRL_PROP_CONTAINER]      = { BAS_PROP_CONTAINER,      BAS_PROP_OBJECT,  true, true  },
    +    [CTRL_PROP_TAG]            = { BAS_PROP_TAG,            WGT_IFACE_STRING, true, true  },
    +    [CTRL_PROP_MOUSEPOINTER]   = { BAS_PROP_MOUSEPOINTER,   WGT_IFACE_INT,    true, true  },
    +};
    +
    +// The Font properties (formrt.h) in .frm order, and the FONT_STYLE_*
    +// flag each Boolean one stands for.
    +typedef enum {
    +    FONT_PROP_NAME,
    +    FONT_PROP_SIZE,
    +    FONT_PROP_BOLD,
    +    FONT_PROP_ITALIC,
    +    FONT_PROP_UNDERLINE,
    +    FONT_PROP_STRIKETHRU,
    +    FONT_PROP_COUNT
    +} BasFontPropE;
    +
    +typedef struct {
    +    const char *name;
    +    uint32_t    style;
    +} BasFontPropT;
    +
    +static const BasFontPropT sFontProps[FONT_PROP_COUNT] = {
    +    [FONT_PROP_NAME]       = { BAS_PROP_FONTNAME,       0                    },
    +    [FONT_PROP_SIZE]       = { BAS_PROP_FONTSIZE,       0                    },
    +    [FONT_PROP_BOLD]       = { BAS_PROP_FONTBOLD,       FONT_STYLE_BOLD      },
    +    [FONT_PROP_ITALIC]     = { BAS_PROP_FONTITALIC,     FONT_STYLE_ITALIC    },
    +    [FONT_PROP_UNDERLINE]  = { BAS_PROP_FONTUNDERLINE,  FONT_STYLE_UNDERLINE },
    +    [FONT_PROP_STRIKETHRU] = { BAS_PROP_FONTSTRIKETHRU, FONT_STYLE_STRIKE    }
     };
     
     // Menu item property ids (MenuName.Prop).  The .frm-only menu keys
    @@ -713,6 +775,11 @@ static BasValueT activeControlOf(BasFormRtT *rt, const BasFormT *onlyForm);
     static BasFormT *allocForm(BasFormRtT *rt, const char *formName, bool resizable, bool centered, int32_t width, int32_t height);
     BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType, uint32_t binaryMask);
     void *basExternResolve(void *ctx, const char *libName, const char *funcName);
    +void basFontChoiceOf(const BitmapFontT *font, DvxFontChoiceT *out);
    +const char *basFontPropAt(int32_t idx);
    +bool basFontPropIs(const char *name);
    +bool basFontPropSetText(DvxFontChoiceT *choice, const char *name, const char *text);
    +bool basFontPropText(const DvxFontChoiceT *choice, const char *name, char *out, int32_t outSize);
     void basFormRtBindVm(BasFormRtT *rt);
     BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc);
     BasFormRtT *basFormRtCodeEnter(BasFormRtT *rt);
    @@ -829,6 +896,10 @@ const char *DlgFileOpen(const char *title, const char *filter);
     const char *DlgFileOpenIn(const char *title, const char *filter, const char *folder);
     const char *DlgFileSave(const char *title, const char *filter);
     const char *DlgFileSaveIn(const char *title, const char *filter, const char *folder);
    +int32_t DlgFont(const char *title, const char *family, int32_t size, int32_t style, int32_t flags);
    +const char *DlgFontName(void);
    +int32_t DlgFontSize(void);
    +int32_t DlgFontStyle(void);
     const char *DlgInputBox(const char *title, const char *prompt, const char *defaultText);
     int32_t DlgInputCancelled(void);
     int32_t DlgIntInput(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal);
    @@ -868,6 +939,9 @@ static bool fireEventArgsOut(BasFormRtT *rt, BasFormT *form, BasControlT *obj, c
     static void firePaint(BasFormRtT *rt, BasControlT *ctrl);
     static void firePaintForm(BasFormRtT *rt, BasFormT *form);
     static void firePaintResized(BasFormRtT *rt, BasControlT *ctrl);
    +static const BasFontPropT *fontPropFind(const char *name);
    +static BasValueT fontPropGet(const WidgetT *w, const char *name);
    +static bool fontPropSet(WidgetT *w, const char *name, BasValueT value);
     static void formatPropNames(const BasPropDescT *table, int32_t count, bool forWrite, char *buf, int32_t bufSize);
     static BasControlT *formEventObject(BasFormT *form, const char *ctrlName);
     static bool formMethod(BasFormRtT *rt, BasFormT *frm, const char *name, BasValueT *args, int32_t argc);
    @@ -878,6 +952,8 @@ static bool formRtDetachTermsForWidget(WidgetT *widget);
     static void formRtSerIdleSync(void);
     int32_t FrameMissed(void);
     static void freeControl(BasControlT *ctrl);
    +static void frmLoadFontFlush(BasFrmLoadCtxT *ctx);
    +static bool frmLoadFontProp(BasFrmLoadCtxT *ctx, WidgetT *w, const char *key, const char *text);
     static void frmLoadOnCtrlBegin(void *userData, const char *typeName, const char *name);
     static void frmLoadOnCtrlEnd(void *userData);
     static void frmLoadOnCtrlProp(void *userData, const char *key, const char *value);
    @@ -1306,6 +1382,63 @@ void *basExternResolve(void *ctx, const char *libName, const char *funcName) {
     }
     
     
    +void basFontChoiceOf(const BitmapFontT *font, DvxFontChoiceT *out) {
    +    snprintf(out->family, sizeof(out->family), "%s", dvxFontFamilyOf(font));
    +    out->size  = font->pixelSize;
    +    out->style = font->style;
    +}
    +
    +
    +const char *basFontPropAt(int32_t idx) {
    +    return (idx >= 0 && idx < FONT_PROP_COUNT) ? sFontProps[idx].name : NULL;
    +}
    +
    +
    +bool basFontPropIs(const char *name) {
    +    return fontPropFind(name) != NULL;
    +}
    +
    +
    +bool basFontPropSetText(DvxFontChoiceT *choice, const char *name, const char *text) {
    +    const BasFontPropT *fp = fontPropFind(name);
    +
    +    if (!fp) {
    +        return false;
    +    }
    +
    +    if (fp == &sFontProps[FONT_PROP_NAME]) {
    +        snprintf(choice->family, sizeof(choice->family), "%s", text);
    +    } else if (fp == &sFontProps[FONT_PROP_SIZE]) {
    +        choice->size = atoi(text);
    +    } else if (frmParseBool(text)) {
    +        choice->style |= fp->style;
    +    } else {
    +        choice->style &= ~fp->style;
    +    }
    +
    +    return true;
    +}
    +
    +
    +bool basFontPropText(const DvxFontChoiceT *choice, const char *name, char *out, int32_t outSize) {
    +    const BasFontPropT *fp = fontPropFind(name);
    +
    +    if (!fp) {
    +        return false;
    +    }
    +
    +    if (fp == &sFontProps[FONT_PROP_NAME]) {
    +        snprintf(out, outSize, "%s", choice->family);
    +    } else if (fp == &sFontProps[FONT_PROP_SIZE]) {
    +        snprintf(out, outSize, "%" PRId32, choice->size);
    +    } else {
    +        snprintf(out, outSize, "%s", (choice->style & fp->style) ? FRM_TEXT_TRUE : FRM_TEXT_FALSE);
    +    }
    +
    +    return true;
    +}
    +
    +
     void basFormRtBindVm(BasFormRtT *rt) {
         BasUiCallbacksT ui;
         memset(&ui, 0, sizeof(ui));
    @@ -1361,7 +1494,7 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName,
         if (pictureFind(rt, ctrlRef)) {
             basFormRtRuntimeError(rt,
                 "Picture has no methods",
    -            "Method: %s\nA Picture object has the read-only properties Width, Height and Type.",
    +            "Method: %s\nA Picture object has the read-only properties Width, Height, and Type.",
                 methodName);
             return zeroValue();
         }
    @@ -2438,6 +2571,9 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
         // that leave may free the form.
         rtEventEnter(rt);
     
    +    // The last block's Font properties
    +    frmLoadFontFlush(&ctx);
    +
         // Apply accumulated form-level properties
         if (form) {
             // Ensure content box exists even if form has no controls.  contentBox
    @@ -2537,7 +2673,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
                             if (!basFormRtMenuLabel(mi->caption, mi->shortcut, mi->shortcutText, label, sizeof(label), &key, &mods)) {
                                 basFormRtRuntimeError(rt,
                                     "Unknown shortcut key",
    -                                "Menu: %s\nShortcut: %s\nUse a key such as F2, Ctrl+X, Alt+Enter or Shift+Del.",
    +                                "Menu: %s\nShortcut: %s\nUse a key such as F2, Ctrl+X, Alt+Enter, or Shift+Del.",
                                     mi->name, mi->shortcut);
                             }
     
    @@ -3497,7 +3633,7 @@ static BasValueT callCommonMethod(BasFormRtT *rt, BasControlT *ctrl, const char
             } else {
                 basFormRtRuntimeError(rt,
                     "Invalid Drag action",
    -                "Control: %s\nAction: %d\nUse 0 (cancel), vbBeginDrag or vbEndDrag.",
    +                "Control: %s\nAction: %d\nUse 0 (cancel), vbBeginDrag, or vbEndDrag.",
                     ctrlDisplayName(ctrl), (int)action);
             }
     
    @@ -4047,7 +4183,7 @@ static BasValueT clipboardMethod(BasFormRtT *rt, const char *name, BasValueT *ar
     
         basFormRtRuntimeError(rt,
             "Clipboard method not supported",
    -        "Method: %s\nThe Clipboard has Clear, GetFormat, GetText and SetText, for text (vbCFText).",
    +        "Method: %s\nThe Clipboard has Clear, GetFormat, GetText, and SetText, for text (vbCFText).",
             name);
         return zeroValue();
     }
    @@ -4803,6 +4939,34 @@ const char *DlgFileSaveIn(const char *title, const char *filter, const char *fol
     }
     
     
    +// The font dialog starting at family, size and style (FONT_STYLE_*
    +// flags; flags DVX_FONT_DLG_FIXED: fixed-pitch families only): True when
    +// the user chose OK.  DlgFontName / DlgFontSize / DlgFontStyle then give
    +// the font chosen, after Cancel the one it started at.
    +int32_t DlgFont(const char *title, const char *family, int32_t size, int32_t style, int32_t flags) {
    +    snprintf(sDlgFont.family, sizeof(sDlgFont.family), "%s", family ? family : "");
    +    sDlgFont.size  = size;
    +    sDlgFont.style = (uint32_t)style & (FONT_STYLE_FACE | FONT_STYLE_LINES);
    +
    +    return (sFormRt && dvxFontDialog(sFormRt->ctx, title, (uint32_t)flags, &sDlgFont)) ? -1 : 0;
    +}
    +
    +
    +const char *DlgFontName(void) {
    +    return sDlgFont.family;
    +}
    +
    +
    +int32_t DlgFontSize(void) {
    +    return sDlgFont.size;
    +}
    +
    +
    +int32_t DlgFontStyle(void) {
    +    return (int32_t)sDlgFont.style;
    +}
    +
    +
     const char *DlgInputBox(const char *title, const char *prompt, const char *defaultText) {
         if (!sFormRt) {
             sLastInputBoxCancelled = true;
    @@ -5558,6 +5722,61 @@ static void firePaintResized(BasFormRtT *rt, BasControlT *ctrl) {
     }
     
     
    +// The Font property entry of name, NULL when name is none.
    +static const BasFontPropT *fontPropFind(const char *name) {
    +    for (int32_t i = 0; i < FONT_PROP_COUNT; i++) {
    +        if (strcasecmp(sFontProps[i].name, name) == 0) {
    +            return &sFontProps[i];
    +        }
    +    }
    +
    +    return NULL;
    +}
    +
    +
    +// Font property name of the font w draws with.
    +static BasValueT fontPropGet(const WidgetT *w, const char *name) {
    +    const BasFontPropT *fp   = fontPropFind(name);
    +    const BitmapFontT  *font = wgtFont(w);
    +
    +    if (fp == &sFontProps[FONT_PROP_NAME]) {
    +        return basValStringFromC(dvxFontFamilyOf(font));
    +    }
    +
    +    if (fp == &sFontProps[FONT_PROP_SIZE]) {
    +        return basValLong(font->pixelSize);
    +    }
    +
    +    return basValBool((font->style & fp->style) != 0);
    +}
    +
    +
    +// Font property name = value: w gets the font it draws with with that
    +// one part changed (the size snaps to one the family has).  False when
    +// out of memory.
    +static bool fontPropSet(WidgetT *w, const char *name, BasValueT value) {
    +    const BasFontPropT *fp = fontPropFind(name);
    +    DvxFontChoiceT      font;
    +
    +    basFontChoiceOf(wgtFont(w), &font);
    +
    +    if (fp == &sFontProps[FONT_PROP_NAME]) {
    +        BasStringT *s = basValFormatString(value);
    +
    +        snprintf(font.family, sizeof(font.family), "%s", s->data);
    +        basStringUnref(s);
    +    } else if (fp == &sFontProps[FONT_PROP_SIZE]) {
    +        font.size = basValToInt32(value);
    +    } else if (basValIsTruthy(value)) {
    +        font.style |= fp->style;
    +    } else {
    +        font.style &= ~fp->style;
    +    }
    +
    +    return wgtSetFont(w, font.family, font.size, font.style);
    +}
    +
    +
     // Comma-separated list of the table's readable (or writable) property
     // names for "Valid: ..." diagnostics.
     static void formatPropNames(const BasPropDescT *table, int32_t count, bool forWrite, char *buf, int32_t bufSize) {
    @@ -5808,9 +6027,42 @@ static void freeControl(BasControlT *ctrl) {
     }
     
     
    +// The Font properties collected for a block (frmLoadFontProp) become its
    +// widget's font.
    +static void frmLoadFontFlush(BasFrmLoadCtxT *ctx) {
    +    if (ctx->fontOwner && !wgtSetFont(ctx->fontOwner, ctx->font.family, ctx->font.size, ctx->font.style)) {
    +        basFormRtRuntimeError(ctx->rt, "Out of memory", "Form: %s\nThere is not enough memory for the font %s.", ctx->form->name, ctx->font.family);
    +    }
    +
    +    ctx->fontOwner = NULL;
    +}
    +
    +
    +// A Font property of the block whose widget is w (a control's, the
    +// form's root): collected, starting from the font w draws with now, and
    +// given to w by frmLoadFontFlush.  False when key is no Font property.
    +static bool frmLoadFontProp(BasFrmLoadCtxT *ctx, WidgetT *w, const char *key, const char *text) {
    +    if (!basFontPropIs(key)) {
    +        return false;
    +    }
    +
    +    if (ctx->fontOwner != w) {
    +        frmLoadFontFlush(ctx);
    +        basFontChoiceOf(wgtFont(w), &ctx->font);
    +        ctx->fontOwner = w;
    +    }
    +
    +    basFontPropSetText(&ctx->font, key, text);
    +    return true;
    +}
    +
    +
     static void frmLoadOnCtrlBegin(void *userData, const char *typeName, const char *name) {
         BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData;
     
    +    // The block before this one has all its Font properties
    +    frmLoadFontFlush(ctx);
    +
         if (!ctx->form || ctx->nestDepth <= 0 || ctx->rt->terminated) {
             return;
         }
    @@ -5889,6 +6141,8 @@ static void frmLoadOnCtrlBegin(void *userData, const char *typeName, const char
     static void frmLoadOnCtrlEnd(void *userData) {
         BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData;
     
    +    frmLoadFontFlush(ctx);
    +
         if (ctx->containerDepth > 0) {
             ctx->containerDepth--;
     
    @@ -5957,6 +6211,10 @@ static void frmLoadOnCtrlProp(void *userData, const char *key, const char *value
             return;
         }
     
    +    if (frmLoadFontProp(ctx, ctrl->widget, key, scratch)) {
    +        return;
    +    }
    +
         // Common runtime-owned properties are typed by the shared table.
         // Anything else goes through as a string so basFormRtSetProp reports
         // the unknown name.
    @@ -5996,8 +6254,9 @@ static bool frmLoadOnFormBegin(void *userData, const char *name) {
     
     // Form-level .frm properties.  Geometry and flags are accumulated on the
     // BasFormT and applied by basFormRtLoadFrm after every control exists
    -// (window fitting needs the final control set); only Caption is
    -// immediate.  The key is validated and typed through sFormProps.
    +// (window fitting needs the final control set), the Font properties are
    +// collected for the root (frmLoadFontProp); only Caption is immediate.
    +// The key is validated and typed through sFormProps.
     static void frmLoadOnFormProp(void *userData, const char *key, const char *value) {
         BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData;
     
    @@ -6087,6 +6346,15 @@ static void frmLoadOnFormProp(void *userData, const char *key, const char *value
                 break;
             }
     
    +        case FORM_PROP_FONTNAME:
    +        case FORM_PROP_FONTSIZE:
    +        case FORM_PROP_FONTBOLD:
    +        case FORM_PROP_FONTITALIC:
    +        case FORM_PROP_FONTUNDERLINE:
    +        case FORM_PROP_FONTSTRIKETHRU:
    +            frmLoadFontProp(ctx, frm->root, pd->name, text);
    +            break;
    +
             case FORM_PROP_NAME:
             case FORM_PROP_VISIBLE:
             case FORM_PROP_CONTROLS:
    @@ -6249,6 +6517,14 @@ static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *ha
             case CTRL_PROP_FORECOLOR:
                 return basValLong((int32_t)wgtBasicColor(w->fgColor));
     
    +        case CTRL_PROP_FONTNAME:
    +        case CTRL_PROP_FONTSIZE:
    +        case CTRL_PROP_FONTBOLD:
    +        case CTRL_PROP_FONTITALIC:
    +        case CTRL_PROP_FONTUNDERLINE:
    +        case CTRL_PROP_FONTSTRIKETHRU:
    +            return fontPropGet(w, pd->name);
    +
             case CTRL_PROP_CAPTION:
             case CTRL_PROP_TEXT: {
                 // Caption and Text both map to the widget's text.
    @@ -6380,7 +6656,13 @@ static BasValueT getFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName
                 return basValObject(pictureFor(rt, frm->iconPath));
     
             case FORM_PROP_TAG:
    -        case FORM_PROP_MOUSEPOINTER: {
    +        case FORM_PROP_MOUSEPOINTER:
    +        case FORM_PROP_FONTNAME:
    +        case FORM_PROP_FONTSIZE:
    +        case FORM_PROP_FONTBOLD:
    +        case FORM_PROP_FONTITALIC:
    +        case FORM_PROP_FONTUNDERLINE:
    +        case FORM_PROP_FONTSTRIKETHRU: {
                 bool handled = false;
     
                 return getCommonProp(&frm->formCtrl, pd->name, &handled);
    @@ -6699,7 +6981,7 @@ static void menuItemShortcut(BasFormRtT *rt, BasFormT *form, BasMenuIdMapT *row,
         if (!basFormRtMenuLabel("", text, NULL, label, sizeof(label), &key, &mods)) {
             basFormRtRuntimeError(rt,
                 "Unknown shortcut key",
    -            "Menu: %s\nShortcut: %s\nUse a key such as F2, Ctrl+X, Alt+Enter or Shift+Del.",
    +            "Menu: %s\nShortcut: %s\nUse a key such as F2, Ctrl+X, Alt+Enter, or Shift+Del.",
                 row->name, text);
             return;
         }
    @@ -8294,7 +8576,7 @@ static BasValueT screenMethod(BasFormRtT *rt, const char *name, BasValueT *args,
             default:
                 basFormRtRuntimeError(rt,
                     "Invalid Arrange value",
    -                "Arrange: %d\nUse vbCascade, vbTileHorizontal, vbTileVertical or vbTileGrid.",
    +                "Arrange: %d\nUse vbCascade, vbTileHorizontal, vbTileVertical, or vbTileGrid.",
                     (int)how);
                 break;
         }
    @@ -8927,6 +9209,17 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val
                 wgtInvalidatePaint(w);
                 break;
     
    +        case CTRL_PROP_FONTNAME:
    +        case CTRL_PROP_FONTSIZE:
    +        case CTRL_PROP_FONTBOLD:
    +        case CTRL_PROP_FONTITALIC:
    +        case CTRL_PROP_FONTUNDERLINE:
    +        case CTRL_PROP_FONTSTRIKETHRU:
    +            if (!fontPropSet(w, pd->name, value)) {
    +                basFormRtRuntimeError(sFormRt, "Out of memory", "Control: %s\nProperty: %s\nThere is not enough memory for the font.", ctrlDisplayName(ctrl), pd->name);
    +            }
    +            break;
    +
             case CTRL_PROP_CAPTION:
             case CTRL_PROP_TEXT: {
                 // Widgets strdup their text internally.
    @@ -9171,6 +9464,12 @@ static void setFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName, Bas
     
             case FORM_PROP_TAG:
             case FORM_PROP_MOUSEPOINTER:
    +        case FORM_PROP_FONTNAME:
    +        case FORM_PROP_FONTSIZE:
    +        case FORM_PROP_FONTBOLD:
    +        case FORM_PROP_FONTITALIC:
    +        case FORM_PROP_FONTUNDERLINE:
    +        case FORM_PROP_FONTSTRIKETHRU:
                 setCommonProp(&frm->formCtrl, pd->name, value);
                 break;
     
    @@ -9254,7 +9553,7 @@ static void setPropValue(BasFormRtT *rt, void *ctrlRef, const char *propName, Ba
         if (ctrlRef == &sClipboardObj) {
             basFormRtRuntimeError(rt,
                 "Clipboard has no properties",
    -            "Property: %s\nUse the Clipboard methods Clear, GetFormat, GetText and SetText.",
    +            "Property: %s\nUse the Clipboard methods Clear, GetFormat, GetText, and SetText.",
                 propName);
             return;
         }
    @@ -9267,7 +9566,7 @@ static void setPropValue(BasFormRtT *rt, void *ctrlRef, const char *propName, Ba
         if (pictureFind(rt, ctrlRef)) {
             basFormRtRuntimeError(rt,
                 "Picture properties are read-only",
    -            "Property: %s\nA Picture object's Width, Height and Type can only be read.",
    +            "Property: %s\nA Picture object's Width, Height, and Type can only be read.",
                 propName);
             return;
         }
    diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.h b/src/apps/kpunch/dvxbasic/formrt/formrt.h
    index 0bdce489..e195e264 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/formrt.h
    +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.h
    @@ -36,6 +36,7 @@
     #include "../runtime/values.h"
     #include "../../../../libs/kpunch/game/game.h"
     #include "dvxApp.h"
    +#include "dvxDlg.h"
     #include "dvxWgt.h"
     #include "frmParser.h"
     #include "../compiler/basEvents.h"
    @@ -104,6 +105,12 @@ typedef struct BasScreenWinT BasScreenWinT;    // the program screen's window (f
     #define BAS_PROP_DRAGICON        "DragIcon"
     #define BAS_PROP_DRAGMODE        "DragMode"
     #define BAS_PROP_ENABLED         "Enabled"
    +#define BAS_PROP_FONTBOLD        "FontBold"
    +#define BAS_PROP_FONTITALIC      "FontItalic"
    +#define BAS_PROP_FONTNAME        "FontName"
    +#define BAS_PROP_FONTSIZE        "FontSize"
    +#define BAS_PROP_FONTSTRIKETHRU  "FontStrikethru"
    +#define BAS_PROP_FONTUNDERLINE   "FontUnderline"
     #define BAS_PROP_FORECOLOR       "ForeColor"
     #define BAS_PROP_HEIGHT          "Height"
     #define BAS_PROP_HELPTOPIC       "HelpTopic"
    @@ -179,6 +186,38 @@ const BasPropDescT *basFormRtFindCommonProp(const char *name);
     const BasPropDescT *basFormRtFindFormProp(const char *name);
     const BasPropDescT *basFormRtFindMenuProp(const char *name);
     
    +// ============================================================
    +// Font properties
    +// ============================================================
    +//
    +// FontName, FontSize, FontBold, FontItalic, FontUnderline and
    +// FontStrikethru (VB's names, in both property tables) are one font: the
    +// family, the size in pixels and the FONT_STYLE_* flags of a
    +// DvxFontChoiceT.  The font itself is the only place they live: reading
    +// one reads the font the object draws with (wgtFont: its own, else its
    +// container's; a form's is its root widget's), writing one gives the
    +// object that font with the one part changed (the size snapping to one
    +// the family has).  These turn a font into the properties' .frm text
    +// and back, for the .frm loader and the designers.
    +
    +// Font property idx in .frm order (FontName, FontSize, then the flags),
    +// NULL past the end.
    +const char *basFontPropAt(int32_t idx);
    +
    +// True when name (any case) is a Font property.
    +bool basFontPropIs(const char *name);
    +
    +// font as the Font properties name it: its family, pixel size and style.
    +void basFontChoiceOf(const BitmapFontT *font, DvxFontChoiceT *out);
    +
    +// Font property name of choice as .frm text: the family, the size, True
    +// or False.  False when name is not a Font property.
    +bool basFontPropText(const DvxFontChoiceT *choice, const char *name, char *out, int32_t outSize);
    +
    +// Set Font property name of choice from .frm text.  False when name is
    +// not a Font property.
    +bool basFontPropSetText(DvxFontChoiceT *choice, const char *name, const char *text);
    +
     // ============================================================
     // Common methods
     // ============================================================
    diff --git a/src/apps/kpunch/dvxbasic/formrt/frmModel.c b/src/apps/kpunch/dvxbasic/formrt/frmModel.c
    index d67bd72a..5706390d 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/frmModel.c
    +++ b/src/apps/kpunch/dvxbasic/formrt/frmModel.c
    @@ -58,8 +58,6 @@
     #define FRM_MENU_SEP_PREFIX   "mnuSep"       // generated separator name prefix
     #define FRM_MENU_NAME_PREFIX  "mnu"          // generated item name prefix
     #define FRM_INT_TEXT_MAX      16             // decimal int32_t text
    -#define FRM_TRUE_TEXT         "True"
    -#define FRM_FALSE_TEXT        "False"
     #define FRM_PROP_LEVEL        "Level"        // a menu item's level (frmMenuPropText)
     
     // ============================================================
    @@ -285,8 +283,8 @@ static int32_t emitClampedV(char *buf, int32_t bufSize, int32_t pos, const char
     
     // "Begin Type Name" and every property line of one control, without the
     // closing End so the writer can nest the children first.  Interface-typed
    -// keys are written by their type; only keys the widget interface does not
    -// know go out as quoted strings.
    +// keys and the runtime's common properties are written by their type;
    +// only other keys go out as quoted strings.
     static int32_t emitControlHead(const FrmCtrlT *ctrl, char *buf, int32_t bufSize, int32_t pos, const char *pad) {
         pos = frmEmitClamped(buf, bufSize, pos, "%sBegin %s %s\n", pad, ctrl->typeName, ctrl->name);
     
    @@ -326,22 +324,24 @@ static int32_t emitControlHead(const FrmCtrlT *ctrl, char *buf, int32_t bufSize,
         }
     
         if (!ctrl->visible && !frmIfaceProp(ctrl->typeName, BAS_PROP_VISIBLE)) {
    -        pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_VISIBLE, WGT_IFACE_BOOL, FRM_FALSE_TEXT);
    +        pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_VISIBLE, WGT_IFACE_BOOL, FRM_TEXT_FALSE);
         }
     
         if (!ctrl->enabled && !frmIfaceProp(ctrl->typeName, BAS_PROP_ENABLED)) {
    -        pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_ENABLED, WGT_IFACE_BOOL, FRM_FALSE_TEXT);
    +        pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_ENABLED, WGT_IFACE_BOOL, FRM_TEXT_FALSE);
         }
     
    -    // Keys the widget interface does not know: raw strings, quoted.
    +    // Keys the widget interface does not know: the runtime's common
    +    // properties by their type, anything else as a quoted string.
         for (int32_t j = 0; j < (int32_t)arrlen(ctrl->props); j++) {
    -        const char *name = ctrl->props[j].name;
    +        const char         *name   = ctrl->props[j].name;
    +        const BasPropDescT *common = basFormRtFindCommonProp(name);
     
             if (strcasecmp(name, BAS_PROP_CAPTION) == 0 || strcasecmp(name, BAS_PROP_TEXT) == 0 || frmIfaceProp(ctrl->typeName, name)) {
                 continue;
             }
     
    -        pos = emitPropLine(buf, bufSize, pos, pad, name, WGT_IFACE_STRING, ctrl->props[j].value);
    +        pos = emitPropLine(buf, bufSize, pos, pad, name, (common && common->type != BAS_PROP_OBJECT) ? common->type : WGT_IFACE_STRING, ctrl->props[j].value);
         }
     
         // Interface properties that were set, typed, in interface order.
    @@ -386,7 +386,7 @@ static int32_t emitPropLine(char *buf, int32_t bufSize, int32_t pos, const char
                 end = frmEmitClamped(buf, bufSize, pos, "%s    %s = \"%s\"\n", pad, name, value);
                 break;
             case WGT_IFACE_BOOL:
    -            end = frmEmitClamped(buf, bufSize, pos, "%s    %s = %s\n", pad, name, frmParseBool(value) ? FRM_TRUE_TEXT : FRM_FALSE_TEXT);
    +            end = frmEmitClamped(buf, bufSize, pos, "%s    %s = %s\n", pad, name, frmParseBool(value) ? FRM_TEXT_TRUE : FRM_TEXT_FALSE);
                 break;
             case WGT_IFACE_INT:
                 end = frmEmitClamped(buf, bufSize, pos, "%s    %s = %d\n", pad, name, atoi(value));
    @@ -489,7 +489,7 @@ static bool formFieldText(const FrmModelT *m, const char *name, char *out, int32
         } else if (isNum) {
             snprintf(out, outSize, "%" PRId32, num);
         } else if (isFlag) {
    -        snprintf(out, outSize, "%s", flag ? FRM_TRUE_TEXT : FRM_FALSE_TEXT);
    +        snprintf(out, outSize, "%s", flag ? FRM_TEXT_TRUE : FRM_TEXT_FALSE);
         } else {
             return false;
         }
    @@ -642,9 +642,9 @@ bool frmCtrlPropText(const FrmCtrlT *ctrl, const char *name, char *out, int32_t
         } else if (strcasecmp(name, BAS_PROP_HELPTOPIC) == 0) {
             snprintf(out, outSize, "%s", ctrl->helpTopic);
         } else if (strcasecmp(name, BAS_PROP_VISIBLE) == 0 && !frmIfaceProp(ctrl->typeName, BAS_PROP_VISIBLE)) {
    -        snprintf(out, outSize, "%s", ctrl->visible ? FRM_TRUE_TEXT : FRM_FALSE_TEXT);
    +        snprintf(out, outSize, "%s", ctrl->visible ? FRM_TEXT_TRUE : FRM_TEXT_FALSE);
         } else if (strcasecmp(name, BAS_PROP_ENABLED) == 0 && !frmIfaceProp(ctrl->typeName, BAS_PROP_ENABLED)) {
    -        snprintf(out, outSize, "%s", ctrl->enabled ? FRM_TRUE_TEXT : FRM_FALSE_TEXT);
    +        snprintf(out, outSize, "%s", ctrl->enabled ? FRM_TEXT_TRUE : FRM_TEXT_FALSE);
         } else if (stored) {
             snprintf(out, outSize, "%s", stored);
         } else {
    @@ -931,7 +931,7 @@ bool frmMenuPropText(const FrmMenuItemT *mi, const char *key, char *out, int32_t
         if (text) {
             snprintf(out, outSize, "%s", text);
         } else if (flag) {
    -        snprintf(out, outSize, "%s", *flag ? FRM_TRUE_TEXT : FRM_FALSE_TEXT);
    +        snprintf(out, outSize, "%s", *flag ? FRM_TEXT_TRUE : FRM_TEXT_FALSE);
         } else if (strcasecmp(key, FRM_PROP_LEVEL) == 0) {
             snprintf(out, outSize, "%" PRId32, mi->level);
         } else {
    @@ -1221,7 +1221,7 @@ bool frmModelFormProp(const FrmModelT *m, const char *name, char *out, int32_t o
         if (i >= 0) {
             snprintf(out, outSize, "%s", m->props[i].value);
         } else if (pd->type == WGT_IFACE_BOOL) {
    -        snprintf(out, outSize, "%s", FRM_FALSE_TEXT);
    +        snprintf(out, outSize, "%s", FRM_TEXT_FALSE);
         } else if (pd->type == WGT_IFACE_STRING) {
             snprintf(out, outSize, "%s", "");
         } else {
    @@ -1508,7 +1508,7 @@ int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, FrmI
             }
     
             if (items[i].shortcut[0] && !dvxAccelParse(items[i].shortcut, &key, &mods)) {
    -            snprintf(reason, reasonSize, "Unknown shortcut key: %s (use a key such as F2, Ctrl+X, Alt+Enter or Shift+Del)", items[i].shortcut);
    +            snprintf(reason, reasonSize, "Unknown shortcut key: %s (use a key such as F2, Ctrl+X, Alt+Enter, or Shift+Del)", items[i].shortcut);
                 return i;
             }
     
    @@ -1644,7 +1644,7 @@ void frmModelOptionSelected(FrmModelT *m, FrmCtrlT *ctrl) {
             const char *other = frmCtrlGetProp(c, BAS_PROP_VALUE);
     
             if (c != ctrl && other && frmParseBool(other) && strcasecmp(c->typeName, FRM_OPTION_TYPE) == 0 && frmModelFindParent(m, c) == parent) {
    -            frmCtrlSetProp(c, BAS_PROP_VALUE, FRM_FALSE_TEXT);
    +            frmCtrlSetProp(c, BAS_PROP_VALUE, FRM_TEXT_FALSE);
             }
         }
     }
    @@ -2386,11 +2386,11 @@ static int32_t writeMenus(const FrmModelT *m, char *buf, int32_t bufSize, int32_
             pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_CAPTION, WGT_IFACE_STRING, mi->caption);
     
             if (mi->checked || mi->checkable) {
    -            pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_CHECKED, WGT_IFACE_BOOL, mi->checked ? FRM_TRUE_TEXT : FRM_FALSE_TEXT);
    +            pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_CHECKED, WGT_IFACE_BOOL, mi->checked ? FRM_TEXT_TRUE : FRM_TEXT_FALSE);
             }
     
             if (mi->radioCheck) {
    -            pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_RADIOCHECK, WGT_IFACE_BOOL, FRM_TRUE_TEXT);
    +            pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_RADIOCHECK, WGT_IFACE_BOOL, FRM_TEXT_TRUE);
             }
     
             if (mi->shortcut[0]) {
    @@ -2402,12 +2402,12 @@ static int32_t writeMenus(const FrmModelT *m, char *buf, int32_t bufSize, int32_
             }
     
             if (!mi->enabled) {
    -            pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_ENABLED, WGT_IFACE_BOOL, FRM_FALSE_TEXT);
    +            pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_ENABLED, WGT_IFACE_BOOL, FRM_TEXT_FALSE);
             }
     
             // Visible matters only on top-level menus: False makes a popup
             if (mi->level == 0 && !mi->visible) {
    -            pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_VISIBLE, WGT_IFACE_BOOL, FRM_FALSE_TEXT);
    +            pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_VISIBLE, WGT_IFACE_BOOL, FRM_TEXT_FALSE);
             }
     
             curLevel = mi->level + 1;
    diff --git a/src/apps/kpunch/dvxbasic/formrt/frmModelBas.c b/src/apps/kpunch/dvxbasic/formrt/frmModelBas.c
    index 529bb192..2c537659 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/frmModelBas.c
    +++ b/src/apps/kpunch/dvxbasic/formrt/frmModelBas.c
    @@ -53,8 +53,6 @@
     #define FRM_SCRATCH_SIZE    64           // the hidden window new controls are made in to read their defaults
     #define FRM_SCRATCH_TITLE   ""
     #define SHELL_CLEANUP_SYM   "shellRegisterAppCleanup"
    -#define FRM_BAS_TRUE_TEXT   "True"
    -#define FRM_BAS_FALSE_TEXT  "False"
     
     typedef void (*FrmCleanupFnT)(int32_t appId);
     typedef void (*FrmCleanupRegFnT)(FrmCleanupFnT fn);
    @@ -672,7 +670,8 @@ static void liveCtrl(FrmLiveT *live, BasControlT *ctrl, FrmCtrlT *parent) {
     // The properties of ctrl that differ from a new control of its type:
     // interface properties by comparison with a new control made in the
     // scratch window, Caption / Text when the control keeps text and it
    -// differs, the other common properties when not empty.
    +// differs, the Font properties when it has a font of its own, the other
    +// common properties when not empty.
     static void liveDefaults(FrmLiveT *live, BasControlT *ctrl, FrmCtrlT *fc) {
         WidgetT         *w     = ctrl->widget;
         const WgtIfaceT *iface = ctrl->iface;
    @@ -726,10 +725,11 @@ static void liveDefaults(FrmLiveT *live, BasControlT *ctrl, FrmCtrlT *fc) {
         const BasPropDescT *common = basFormRtCommonProps(&count);
     
         for (int32_t i = 0; i < count; i++) {
    -        const BasPropDescT *pd    = &common[i];
    -        bool                fixed = strcasecmp(pd->name, BAS_PROP_NAME) == 0 || strcasecmp(pd->name, BAS_PROP_CAPTION) == 0 || strcasecmp(pd->name, BAS_PROP_TEXT) == 0 || strcasecmp(pd->name, BAS_PROP_HELPTOPIC) == 0 || frmIntPropFind(pd->name);
    +        const BasPropDescT *pd     = &common[i];
    +        bool                fixed  = strcasecmp(pd->name, BAS_PROP_NAME) == 0 || strcasecmp(pd->name, BAS_PROP_CAPTION) == 0 || strcasecmp(pd->name, BAS_PROP_TEXT) == 0 || strcasecmp(pd->name, BAS_PROP_HELPTOPIC) == 0 || frmIntPropFind(pd->name);
    +        bool                isFont = basFontPropIs(pd->name);
     
    -        if (fixed || !pd->writable || pd->type == BAS_PROP_OBJECT || wgtIfaceFindProp(iface, pd->name)) {
    +        if (fixed || !pd->writable || pd->type == BAS_PROP_OBJECT || wgtIfaceFindProp(iface, pd->name) || (isFont && (!w || !w->font))) {
                 continue;
             }
     
    @@ -742,7 +742,7 @@ static void liveDefaults(FrmLiveT *live, BasControlT *ctrl, FrmCtrlT *fc) {
                 fc->visible = frmParseBool(val);
             } else if (strcasecmp(pd->name, BAS_PROP_ENABLED) == 0) {
                 fc->enabled = frmParseBool(val);
    -        } else if (pd->type == WGT_IFACE_STRING ? val[0] != '\0' : (pd->type == WGT_IFACE_BOOL ? frmParseBool(val) : atoi(val) != 0)) {
    +        } else if (isFont || (pd->type == WGT_IFACE_STRING ? val[0] != '\0' : (pd->type == WGT_IFACE_BOOL ? frmParseBool(val) : atoi(val) != 0))) {
                 frmCtrlSetProp(fc, pd->name, val);
             }
         }
    @@ -759,8 +759,9 @@ static void liveForm(FrmLiveT *live) {
         for (int32_t i = 0; i < count; i++) {
             const BasPropDescT *pd = &props[i];
     
    -        // Name is the Begin line's; the rest only when the model keeps it
    -        if (strcasecmp(pd->name, BAS_PROP_NAME) == 0 || !frmModelFormProp(live->m, pd->name, val, sizeof(val))) {
    +        // Name is the Begin line's; the rest only when the model keeps it,
    +        // the Font properties when the form has a font of its own
    +        if (strcasecmp(pd->name, BAS_PROP_NAME) == 0 || !frmModelFormProp(live->m, pd->name, val, sizeof(val)) || (basFontPropIs(pd->name) && !form->root->font)) {
                 continue;
             }
     
    @@ -920,7 +921,7 @@ static void valueText(BasValueT v, uint8_t type, char *out, int32_t outSize) {
             snprintf(out, outSize, "%s", s->data);
             basStringUnref(s);
         } else if (type == WGT_IFACE_BOOL) {
    -        snprintf(out, outSize, "%s", basValIsTruthy(v) ? FRM_BAS_TRUE_TEXT : FRM_BAS_FALSE_TEXT);
    +        snprintf(out, outSize, "%s", basValIsTruthy(v) ? FRM_TEXT_TRUE : FRM_TEXT_FALSE);
         } else {
             snprintf(out, outSize, "%" PRId32, (int32_t)basValToNumber(v));
         }
    diff --git a/src/apps/kpunch/dvxbasic/formrt/frmParser.h b/src/apps/kpunch/dvxbasic/formrt/frmParser.h
    index 633fbdad..1ff2dbd2 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/frmParser.h
    +++ b/src/apps/kpunch/dvxbasic/formrt/frmParser.h
    @@ -59,6 +59,10 @@
     // the form model also returns it for "no such control / menu item".
     #define FRM_NO_INDEX          (-1)
     
    +// A Boolean value as the .frm writers put it (frmParseBool reads it back).
    +#define FRM_TEXT_TRUE         "True"
    +#define FRM_TEXT_FALSE        "False"
    +
     // Callbacks supplied by the consumer. Any field may be NULL.
     typedef struct FrmParserCbsT {
         void *userData;
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
    index f1133d4e..5c0cad31 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
    +++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
    @@ -55,11 +55,14 @@
     static WidgetT            *containerChildParent(FrmCtrlT *pc);
     static WidgetT            *createDesignWidget(const char *vbTypeName, WidgetT *parent);
     void                       dsgnApplyEnabled(const FrmModelT *form);
    +void                       dsgnApplyFont(WidgetT *w, const FrmPropT *props);
    +void                       dsgnApplyFonts(DsgnStateT *ds);
     void                       dsgnApplyGaps(DsgnStateT *ds);
     void                       dsgnBuildPreviewMenuBar(WindowT *win, const FrmModelT *form);
     WidgetT                   *dsgnCreateControlWidget(const FrmModelT *form, FrmCtrlT *ctrl, WidgetT *parent);
     void                       dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox);
     void                       dsgnFitFormWin(DsgnStateT *ds);
    +WidgetT                   *dsgnFormWidget(const DsgnStateT *ds);
     void                       dsgnFree(DsgnStateT *ds);
     void                       dsgnInit(DsgnStateT *ds, AppContextT *ctx);
     bool                       dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen);
    @@ -141,6 +144,44 @@ void dsgnApplyEnabled(const FrmModelT *form) {
     }
     
     
    +void dsgnApplyFont(WidgetT *w, const FrmPropT *props) {
    +    DvxFontChoiceT font;
    +    bool           own = false;
    +
    +    wgtSetFont(w, NULL, 0, 0);
    +    basFontChoiceOf(wgtFont(w), &font);
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(props); i++) {
    +        if (basFontPropSetText(&font, props[i].name, props[i].value)) {
    +            own = true;
    +        }
    +    }
    +
    +    if (own) {
    +        wgtSetFont(w, font.family, font.size, font.style);
    +    }
    +}
    +
    +
    +void dsgnApplyFonts(DsgnStateT *ds) {
    +    WidgetT *root = dsgnFormWidget(ds);
    +
    +    if (!ds->form || !root) {
    +        return;
    +    }
    +
    +    dsgnApplyFont(root, ds->form->props);
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(ds->form->controls); i++) {
    +        FrmCtrlT *ctrl = ds->form->controls[i];
    +
    +        if (dsgnWidget(ctrl)) {
    +            dsgnApplyFont(dsgnWidget(ctrl), ctrl->props);
    +        }
    +    }
    +}
    +
    +
     void dsgnApplyGaps(DsgnStateT *ds) {
         WidgetT *box = ds->contentBox;
     
    @@ -218,11 +259,12 @@ void dsgnBuildPreviewMenuBar(WindowT *win, const FrmModelT *form) {
     
     
     // Create one control's live design widget under parent and apply its
    -// name, text, geometry, enabled state and interface properties.  Visible is
    -// deliberately not applied: a hidden widget gets no layout geometry, which
    -// would make a Visible=False control invisible and unclickable on the
    -// canvas.  The runtime loader applies it instead.  Returns the widget, or
    -// NULL when the type cannot be created at design time.
    +// name, text, geometry, enabled state, interface properties and font.
    +// Visible is deliberately not applied: a hidden widget gets no layout
    +// geometry, which would make a Visible=False control invisible and
    +// unclickable on the canvas.  The runtime loader applies it instead.
    +// Returns the widget, or NULL when the type cannot be created at design
    +// time.
     WidgetT *dsgnCreateControlWidget(const FrmModelT *form, FrmCtrlT *ctrl, WidgetT *parent) {
         WidgetT *w = createDesignWidget(ctrl->typeName, parent);
     
    @@ -259,6 +301,7 @@ WidgetT *dsgnCreateControlWidget(const FrmModelT *form, FrmCtrlT *ctrl, WidgetT
             }
         }
     
    +    dsgnApplyFont(w, ctrl->props);
         return w;
     }
     
    @@ -273,6 +316,7 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
     
         ds->contentBox = contentBox;
         dsgnApplyGaps(ds);
    +    dsgnApplyFont(dsgnFormWidget(ds), ds->form->props);
     
         int32_t count = (int32_t)arrlen(ds->form->controls);
     
    @@ -301,6 +345,17 @@ void dsgnFitFormWin(DsgnStateT *ds) {
     }
     
     
    +WidgetT *dsgnFormWidget(const DsgnStateT *ds) {
    +    WidgetT *root = ds->contentBox;
    +
    +    while (root && root->parent) {
    +        root = root->parent;
    +    }
    +
    +    return root;
    +}
    +
    +
     void dsgnFree(DsgnStateT *ds) {
         frmModelFree(ds->form);
         ds->form = NULL;
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
    index 50d1f5a4..ed2a8089 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
    +++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
    @@ -87,13 +87,14 @@ static inline WidgetT *dsgnWidget(const FrmCtrlT *ctrl);
     // Initialize designer state.
     void dsgnInit(DsgnStateT *ds, AppContextT *ctx);
     
    -// Create live widgets for all controls in the form that have none.
    -// Call after dsgnLoadFrm or dsgnNewForm, with the form window's contentBox.
    +// Put the form's gaps and font on its window, then create live widgets
    +// for all controls in the form that have none.  Call after dsgnLoadFrm
    +// or dsgnNewForm, with the form window's contentBox.
     void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox);
     
     // Create one control's live design widget under parent (name, text,
    -// geometry, enabled state and interface properties from the control's
    -// data; Visible is left to the runtime).  Sets and returns the widget,
    +// geometry, enabled state, interface properties and font from the
    +// control's data; Visible is left to the runtime).  Sets and returns the widget,
     // NULL when the type cannot be created at design time.  form resolves the
     // control's ancestors for the effective enabled state.
     WidgetT *dsgnCreateControlWidget(const FrmModelT *form, FrmCtrlT *ctrl, WidgetT *parent);
    @@ -102,6 +103,16 @@ WidgetT *dsgnCreateControlWidget(const FrmModelT *form, FrmCtrlT *ctrl, WidgetT
     // ancestors') to the live widgets.
     void dsgnApplyEnabled(const FrmModelT *form);
     
    +// Give w (a control's live widget, or dsgnFormWidget) the font its
    +// stored Font properties among props make over the font it inherits;
    +// with none stored it inherits.
    +void dsgnApplyFont(WidgetT *w, const FrmPropT *props);
    +
    +// dsgnApplyFont for the form, then for every control with a live widget
    +// in tree order (a container's font before the fonts of the controls
    +// inside it, which build on it).
    +void dsgnApplyFonts(DsgnStateT *ds);
    +
     // The form's Padding / Spacing on the content box (one the form does not
     // set: the layout's own), then a re-layout.
     void dsgnApplyGaps(DsgnStateT *ds);
    @@ -139,6 +150,10 @@ void dsgnPaintOverlay(DsgnStateT *ds);
     // The selected control's widget, NULL when the form is selected.
     WidgetT *dsgnSelectedWidget(const DsgnStateT *ds);
     
    +// The widget holding the form's font: the form window's root, as at run
    +// time (NULL without a window).
    +WidgetT *dsgnFormWidget(const DsgnStateT *ds);
    +
     // Handle mouse click on the design surface.
     void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag);
     
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideMain.c b/src/apps/kpunch/dvxbasic/ide/ideMain.c
    index df0c247a..8a7c697f 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideMain.c
    +++ b/src/apps/kpunch/dvxbasic/ide/ideMain.c
    @@ -31,6 +31,7 @@
     #include "dvxPlat.h"
     #include "dvxDlg.h"
     #include "dvxPrefs.h"
    +#include "dvxText.h"
     #include "dvxWgt.h"
     #include "dvxWgtP.h"
     #include "dvxWm.h"
    @@ -604,6 +605,7 @@ static bool renameProjectFileTo(int32_t fileIdx, const char *newName);
     static int32_t replaceInSections(const char *objFilter, bool caseSensitive);
     static void reportUnreadable(const char *path);
     static void runCached(void);
    +static void runFontsAdd(int32_t owner);
     static void runModule(BasModuleT *mod);
     static BasVmResultE runProgramSlices(BasFormRtT *rt);
     static bool saveActiveFile(void);
    @@ -4251,6 +4253,11 @@ static void makeExecutable(void) {
         spec.ctlCount      = (int32_t)arrlen(ctlSources);
         spec.release       = release;
         spec.multiInstance = sProject.multiInstance;
    +    spec.fontCount     = prjFontCount(&sProject);
    +
    +    for (int32_t i = 0; i < spec.fontCount && i < BAS_BUILD_FONTS_MAX; i++) {
    +        spec.fonts[i] = prjFontPath(&sProject, i);
    +    }
     
         const char *failure = control ? basBuildControl(outPath, &spec) : basBuildApp(outPath, &spec);
     
    @@ -4838,7 +4845,7 @@ static void onCtlHeaderSet(const char *key, const char *value) {
         if (!changed) {
             char msg[IDE_MSG_BUF];
     
    -        snprintf(msg, sizeof(msg), rename ? "The project already has a form, class module or UserControl named %s, or it is not a valid name." : "%s cannot be set to that value.", rename ? value : key);
    +        snprintf(msg, sizeof(msg), rename ? "The project already has a form, class module, or UserControl named %s, or it is not a valid name." : "%s cannot be set to that value.", rename ? value : key);
             ideErrorBox(msg);
             free(owned);
             return;
    @@ -6717,7 +6724,7 @@ static bool renameProjectFileTo(int32_t fileIdx, const char *newName) {
         }
     
         if (nameClass && strcasecmp(newClass, oldClass) != 0 && classNameUsed(newClass, "")) {
    -        snprintf(err, sizeof(err), "The project already has a form, class module or UserControl named %s.", newClass);
    +        snprintf(err, sizeof(err), "The project already has a form, class module, or UserControl named %s.", newClass);
             ideErrorBox(err);
             return false;
         }
    @@ -6813,6 +6820,24 @@ static void runCached(void) {
     }
     
     
    +// The project's fonts added for owner (the IDE) while a program runs; one
    +// that cannot be added is reported in the Output window
    +static void runFontsAdd(int32_t owner) {
    +    for (int32_t i = 0; i < prjFontCount(&sProject); i++) {
    +        char path[DVX_MAX_PATH];
    +
    +        platformPathJoin(path, sizeof(path), sProject.projectDir, prjFontPath(&sProject, i));
    +
    +        if (!dvxFontAddFile(owner, path)) {
    +            char msg[DVX_MAX_PATH + IDE_MSG_BUF];
    +
    +            snprintf(msg, sizeof(msg), "Cannot use the font %s.\n", path);
    +            outputAppend(msg, (int32_t)strlen(msg));
    +        }
    +    }
    +}
    +
    +
     static void runModule(BasModuleT *mod) {
         // Never nest a run: a stopped program may still be unwinding
         if (sVm) {
    @@ -6910,6 +6935,12 @@ static void runModule(BasModuleT *mod) {
             }
         }
     
    +    // The project's fonts, for the program run here as for the built one
    +    // (added for the IDE until the run ends)
    +    int32_t fontOwner = sAc->currentAppId;
    +
    +    runFontsAdd(fontOwner);
    +
         // Load forms from project files (AFTER breakpoints are set so
         // init code breakpoints fire), then show the startup form.
         loadRunFiles(formRt);
    @@ -7017,6 +7048,10 @@ static void runModule(BasModuleT *mod) {
     
         basRunHostDestroy(formRt);
     
    +    // The fonts the run added (the project's and any the program added):
    +    // the IDE carries none of its own
    +    dvxFontRemoveOwner(fontOwner);
    +
         // If the IDE was closed while the program was running, skip all UI
         // updates -- the windows are already destroyed -- and release the
         // module onClose left for us.
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideProject.c b/src/apps/kpunch/dvxbasic/ide/ideProject.c
    index f034c013..995be34d 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideProject.c
    +++ b/src/apps/kpunch/dvxbasic/ide/ideProject.c
    @@ -29,8 +29,10 @@
     
     #include "ideProject.h"
     #include "../basRes.h"
    +#include "../basBuild.h"
     #include "dvxApp.h"
     #include "dvxDlg.h"
    +#include "dvxFontFmt.h"
     #include "dvxPrefs.h"
     #include "dvxWm.h"
     #include "box/box.h"
    @@ -39,6 +41,7 @@
     #include "dropdown/dropdown.h"
     #include "image/image.h"
     #include "label/label.h"
    +#include "listBox/listBox.h"
     #include "textInput/textInpt.h"
     #include "treeView/treeView.h"
     
    @@ -68,6 +71,7 @@
     #define PPD_BTN_H    24
     #define PPD_ROOT_SPACING 2
     #define PPD_ROW_SPACING  4
    +#define PPD_FONT_LIST_H  48    // the Fonts list: about three rows
     
     // Project Properties' Type rows: a program ([Project] Type empty) or a
     // control project (BAS_PROJECT_TYPE_CONTROL)
    @@ -120,6 +124,8 @@ static struct {
         WidgetT      *multiInstance;
         const char  **formNames;    // stb_ds array of form name strings for startup dropdown
         WidgetT      *helpFileInput;
    +    WidgetT      *fontList;
    +    char        **fonts;        // stb_ds array of the font paths listed (strdup'd)
         WidgetT      *iconPreview;
         char          iconPath[DVX_MAX_PATH];
         const char   *appPath;
    @@ -135,12 +141,16 @@ static void        onPrjWinClose(WindowT *win);
     static void        onTreeItemDblClick(WidgetT *w);
     static void        onTreeSelChanged(WidgetT *w);
     static WidgetT    *ppdAddRow(WidgetT *parent, const char *labelText, const char *value, int32_t fieldSize);
    +static void        ppdFontsShow(void);
    +static const char *ppdIntoProject(const char *path, const char *noun, const char *title);
     static void        ppdLoadIconPreview(void);
    +static void        ppdOnAddFont(WidgetT *w);
     static void        ppdOnBrowseHelp(WidgetT *w);
     static void        ppdOnBrowseIcon(WidgetT *w);
     static void        ppdOnCancel(WidgetT *w);
     static void        ppdOnClose(WindowT *win);
     static void        ppdOnOk(WidgetT *w);
    +static void        ppdOnRemoveFont(WidgetT *w);
     static void        ppdReadInput(WidgetT *input, char *dst, int32_t dstSize);
     void               prjApplyDefaults(PrjStateT *prj, PrefsHandleT *prefs);
     WindowT           *prjCreateWindow(AppContextT *ctx, PrjStateT *prj, PrjFileClickFnT onClick, PrjSelChangeFnT onSelChange);
    @@ -197,6 +207,53 @@ static WidgetT *ppdAddRow(WidgetT *parent, const char *labelText, const char *va
     }
     
     
    +// The Fonts list from sPpd.fonts
    +static void ppdFontsShow(void) {
    +    wgtListBoxSetItems(sPpd.fontList, (const char **)sPpd.fonts, (int32_t)arrlen(sPpd.fonts));
    +}
    +
    +
    +// path as the project knows it (relative to its folder); a file outside
    +// the folder is copied into it first when the user agrees (noun names
    +// it in the questions, title heads them).  NULL when it is not to be
    +// used.  The result points into path.
    +static const char *ppdIntoProject(const char *path, const char *noun, const char *title) {
    +    const char *relPath = prjMakeRelative(sPpd.prj, path);
    +    char        msg[DVX_MAX_PATH + PRJ_MSG_BUF];
    +
    +    if (relPath) {
    +        return relPath;
    +    }
    +
    +    snprintf(msg, sizeof(msg), "The %s is outside the project directory.\nCopy it to the project?", noun);
    +
    +    if (dvxMessageBox(sPpd.ctx, title, msg, MB_YESNO | MB_ICONQUESTION) != ID_YES) {
    +        return NULL;
    +    }
    +
    +    const char *fname = platformPathBaseName(path);
    +    char        destPath[DVX_MAX_PATH * 2];
    +
    +    platformPathJoin(destPath, sizeof(destPath), sPpd.prj->projectDir, fname);
    +
    +    if (prjPathExists(destPath)) {
    +        snprintf(msg, sizeof(msg), "%s already exists.\nOverwrite it?", fname);
    +
    +        if (dvxMessageBox(sPpd.ctx, "Overwrite", msg, MB_YESNO | MB_ICONQUESTION) != ID_YES) {
    +            return NULL;
    +        }
    +    }
    +
    +    if (!platformCopyFile(path, destPath)) {
    +        snprintf(msg, sizeof(msg), "Failed to copy the %s into the project directory.", noun);
    +        dvxErrorBox(sPpd.ctx, NULL, msg);
    +        return NULL;
    +    }
    +
    +    return fname;
    +}
    +
    +
     static void ppdLoadIconPreview(void) {
         if (!sPpd.iconPreview || !sPpd.ctx || !sPpd.prj) {
             return;
    @@ -227,6 +284,54 @@ static void ppdLoadIconPreview(void) {
     }
     
     
    +// Add...: a .DFN font for the program to carry (in the project folder)
    +static void ppdOnAddFont(WidgetT *w) {
    +    (void)w;
    +
    +    FileFilterT filters[] = {
    +        { "DVX Fonts (*.dfn)" },
    +        { "All Files (*.*)" }
    +    };
    +
    +    char       path[DVX_MAX_PATH];
    +    char       err[DFN_ERR_MAX];
    +    DfnSourceT src;
    +    DfnFileT   file;
    +
    +    if (!dvxFileDialog(sPpd.ctx, "Add Font", FD_OPEN, NULL, filters, DVX_ARRAY_LEN(filters), path, sizeof(path))) {
    +        return;
    +    }
    +
    +    dfnSourceFile(&src, path);
    +
    +    if (!dfnOpen(&src, &file, err, sizeof(err))) {
    +        dvxErrorBox(sPpd.ctx, NULL, err);
    +        return;
    +    }
    +
    +    if (arrlen(sPpd.fonts) >= BAS_BUILD_FONTS_MAX) {
    +        char msg[PRJ_MSG_BUF];
    +
    +        snprintf(msg, sizeof(msg), "A program carries at most %d fonts.", BAS_BUILD_FONTS_MAX);
    +        dvxErrorBox(sPpd.ctx, NULL, msg);
    +        return;
    +    }
    +
    +    const char *relPath = ppdIntoProject(path, "font", "Copy Font");
    +
    +    for (int32_t i = 0; relPath && i < (int32_t)arrlen(sPpd.fonts); i++) {
    +        relPath = platformPathSame(sPpd.fonts[i], relPath) ? NULL : relPath;
    +    }
    +
    +    char *copy = relPath ? strdup(relPath) : NULL;
    +
    +    if (copy) {
    +        arrput(sPpd.fonts, copy);
    +        ppdFontsShow();
    +    }
    +}
    +
    +
     static void ppdOnBrowseHelp(WidgetT *w) {
         (void)w;
     
    @@ -258,49 +363,15 @@ static void ppdOnBrowseIcon(WidgetT *w) {
     
         char path[DVX_MAX_PATH];
     
    -    if (dvxFileDialog(sPpd.ctx, "Select Icon", FD_OPEN, NULL, filters, DVX_ARRAY_LEN(filters), path, sizeof(path))) {
    -        if (!validateIcon(path, true)) {
    -            return;
    -        }
    +    if (!dvxFileDialog(sPpd.ctx, "Select Icon", FD_OPEN, NULL, filters, DVX_ARRAY_LEN(filters), path, sizeof(path)) || !validateIcon(path, true)) {
    +        return;
    +    }
     
    -        // The icon must be in the project directory so the relative
    -        // path works when the project is reloaded.
    -        const char *relPath = prjMakeRelative(sPpd.prj, path);
    -
    -        if (!relPath) {
    -            int32_t result = dvxMessageBox(sPpd.ctx, "Copy Icon",
    -                "The icon is outside the project directory.\nCopy it to the project?",
    -                MB_YESNO | MB_ICONQUESTION);
    -
    -            if (result != ID_YES) {
    -                return;
    -            }
    -
    -            // Get just the filename
    -            const char *fname = platformPathBaseName(path);
    -
    -            // Check if destination already exists
    -            char destPath[DVX_MAX_PATH * 2];
    -            platformPathJoin(destPath, sizeof(destPath), sPpd.prj->projectDir, fname);
    -
    -            if (prjPathExists(destPath)) {
    -                char msg[DVX_MAX_PATH + PRJ_MSG_BUF];
    -                snprintf(msg, sizeof(msg), "%s already exists.\nOverwrite it?", fname);
    -                int32_t ow = dvxMessageBox(sPpd.ctx, "Overwrite", msg, MB_YESNO | MB_ICONQUESTION);
    -
    -                if (ow != ID_YES) {
    -                    return;
    -                }
    -            }
    -
    -            if (!platformCopyFile(path, destPath)) {
    -                dvxErrorBox(sPpd.ctx, NULL, "Failed to copy icon into the project directory.");
    -                return;
    -            }
    -
    -            relPath = fname;
    -        }
    +    // The icon must be in the project directory so the relative path
    +    // works when the project is reloaded
    +    const char *relPath = ppdIntoProject(path, "icon", "Copy Icon");
     
    +    if (relPath) {
             snprintf(sPpd.iconPath, sizeof(sPpd.iconPath), "%s", relPath);
             ppdLoadIconPreview();
         }
    @@ -339,6 +410,20 @@ static void ppdOnOk(WidgetT *w) {
     }
     
     
    +// Remove: the font chosen in the list (its file stays)
    +static void ppdOnRemoveFont(WidgetT *w) {
    +    int32_t idx = wgtListBoxGetSelected(sPpd.fontList);
    +
    +    (void)w;
    +
    +    if (idx >= 0 && idx < (int32_t)arrlen(sPpd.fonts)) {
    +        free(sPpd.fonts[idx]);
    +        arrdel(sPpd.fonts, idx);
    +        ppdFontsShow();
    +    }
    +}
    +
    +
     // Copy a dialog input's text into dst (dst keeps its value when the input
     // has none).
     static void ppdReadInput(WidgetT *input, char *dst, int32_t dstSize) {
    @@ -521,7 +606,13 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
                     name = fallback;
                 }
     
    -            arrput(sPpd.formNames, strdup(name));
    +            char *copy = strdup(name);
    +
    +            if (!copy) {
    +                continue;
    +            }
    +
    +            arrput(sPpd.formNames, copy);
     
                 if (strcasecmp(name, prj->startupForm) == 0) {
                     selectedIdx = (int32_t)arrlen(sPpd.formNames) - 1;
    @@ -580,6 +671,36 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
             hlpBrowse->onClick = ppdOnBrowseHelp;
         }
     
    +    // Fonts the program carries: a list with Add and Remove
    +    {
    +        WidgetT *fontRow = wgtHBox(root);
    +        fontRow->spacing = wgtPixels(PPD_ROW_SPACING);
    +
    +        WidgetT *fontLbl = wgtLabel(fontRow, "Fonts:");
    +        fontLbl->minW = wgtPixels(PPD_LABEL_W);
    +
    +        sPpd.fontList         = wgtListBox(fontRow);
    +        sPpd.fontList->weight = WGT_WEIGHT_FILL;
    +        sPpd.fontList->minH   = wgtPixels(PPD_FONT_LIST_H);
    +
    +        WidgetT *fontBtns = wgtVBox(fontRow);
    +        WidgetT *addBtn   = wgtButton(fontBtns, "Add...");
    +        WidgetT *delBtn   = wgtButton(fontBtns, "Remove");
    +
    +        addBtn->onClick = ppdOnAddFont;
    +        delBtn->onClick = ppdOnRemoveFont;
    +
    +        for (int32_t i = 0; i < prjFontCount(prj); i++) {
    +            char *copy = strdup(prjFontPath(prj, i));
    +
    +            if (copy) {
    +                arrput(sPpd.fonts, copy);
    +            }
    +        }
    +
    +        ppdFontsShow();
    +    }
    +
         // Compiler options: OPTION EXPLICIT enforces DIM-before-use for this project.
         sPpd.optionExplicit = wgtCheckbox(root, "Require variable declaration (OPTION EXPLICIT)");
         wgtCheckboxSetChecked(sPpd.optionExplicit, prj->optionExplicit);
    @@ -639,6 +760,14 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
     
             prj->optionExplicit = wgtCheckboxIsChecked(sPpd.optionExplicit);
             prj->multiInstance  = wgtCheckboxIsChecked(sPpd.multiInstance);
    +
    +        while (prjFontCount(prj) > 0) {
    +            prjRemoveFont(prj, 0);
    +        }
    +
    +        for (int32_t i = 0; i < (int32_t)arrlen(sPpd.fonts); i++) {
    +            prjAddFont(prj, sPpd.fonts[i]);
    +        }
             prjFieldSet(prj, BAS_INI_KEY_TYPE, (wgtDropdownGetSelected(sPpd.type) == PpdTypeControlE) ? BAS_PROJECT_TYPE_CONTROL : "");
             prj->dirty = true;
         }
    @@ -650,6 +779,13 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
         arrfree(sPpd.formNames);
         sPpd.formNames = NULL;
     
    +    for (int32_t i = 0; i < (int32_t)arrlen(sPpd.fonts); i++) {
    +        free(sPpd.fonts[i]);
    +    }
    +
    +    arrfree(sPpd.fonts);
    +    sPpd.fonts = NULL;
    +
         ctx->modalWindow = prevModal;
         dvxDestroyWindow(ctx, win);
     
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideProperties.c b/src/apps/kpunch/dvxbasic/ide/ideProperties.c
    index 18bb9f26..17bd230a 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideProperties.c
    +++ b/src/apps/kpunch/dvxbasic/ide/ideProperties.c
    @@ -25,7 +25,8 @@
     // A floating window with a TreeView listing all controls on the
     // form (for selection and drag-reorder) and a ListView showing
     // editable properties of the selected control. Double-click a
    -// property value to edit it via an InputBox dialog.  While a UserControl
    +// property value to edit it via an InputBox dialog (the Font row in the
    +// font dialog: the six Font properties at once).  While a UserControl
     // (.ctl) is the active file the window shows its header instead (Name,
     // Width, Height ... DefaultEvent: prjCtlKeyAt), edited by kind: a number
     // in a spin box, a flag toggled, BackColor in the color dialog, the
    @@ -72,6 +73,7 @@
     #define PRP_NONE_ITEM         "(none)"   // dropdown entry that clears a binding
     #define PRP_DATA_TYPE         "Data"     // Data control type name
     #define PRP_ROW_TYPE          "Type"     // read-only grid row naming the control's type
    +#define PRP_ROW_FONT          "Font"     // grid row of the Font properties, edited in the font dialog
     #define PRP_EDIT_TITLE        "Edit Property"
     #define PRP_CTL_MIN_SIZE      1          // smallest UserControl Width / Height
     #define PRP_CTL_DEF_COLOR     0xFFFFFF   // BackColor the color dialog starts from when the header gives none
    @@ -87,9 +89,23 @@ typedef enum {
         PRP_EDIT_LAYOUT,
         PRP_EDIT_DATASOURCE,
         PRP_EDIT_DATAFIELD,
    -    PRP_EDIT_RECORDSRC
    +    PRP_EDIT_RECORDSRC,
    +    PRP_EDIT_FONT
     } PrpEditE;
     
    +// The Font row's words for the style flags, in the order they show
    +typedef struct {
    +    uint32_t    style;
    +    const char *word;
    +} PrpFontWordT;
    +
    +static const PrpFontWordT sFontWords[] = {
    +    { FONT_STYLE_BOLD,      "Bold"       },
    +    { FONT_STYLE_ITALIC,    "Italic"     },
    +    { FONT_STYLE_UNDERLINE, "Underline"  },
    +    { FONT_STYLE_STRIKE,    "Strikethru" }
    +};
    +
     // No fixed caps: field and table arrays are heap-allocated and sized to the
     // actual count reported by SQL.
     
    @@ -134,11 +150,13 @@ typedef struct {
     // Prototypes
     // ============================================================
     
    +static void               addFontRow(const WidgetT *w);
     static void               addPropRow(const char *name, const char *value);
     static void               collectTreeOrder(WidgetT *parent, FrmCtrlT **srcArr, int32_t srcCount, FrmCtrlT ***outArr, PrpParentT **outParents, FrmCtrlT *treeParent, int32_t depth);
     static void               ctlAddEvent(void *ctx, const char *name, const char *params);
     static void               ctlEditRow(int32_t row);
     static void               ctlShowRows(void);
    +static void               editFont(void);
     static WidgetT            *findTreeItemByName(WidgetT *parent, const char *name, int32_t index);
     static void               freeCellData(void);
     static void               freeTreeLabels(void);
    @@ -156,6 +174,31 @@ static bool               treeOrderMatches(void);
     static bool               validateNewName(const char *name, int32_t maxLen, const char *exceptName);
     
     
    +// The Font row of the object w is the widget of (none without one): the
    +// family, size and styles of the font it draws with ("DejaVu Sans, 14,
    +// Bold").
    +static void addFontRow(const WidgetT *w) {
    +    DvxFontChoiceT font;
    +    char           text[FRM_MAX_TEXT];
    +    int32_t        pos;
    +
    +    if (!w) {
    +        return;
    +    }
    +
    +    basFontChoiceOf(wgtFont(w), &font);
    +    pos = frmEmitClamped(text, sizeof(text), 0, "%s, %" PRId32, font.family, font.size);
    +
    +    for (int32_t i = 0; i < DVX_ARRAY_LEN(sFontWords); i++) {
    +        if (font.style & sFontWords[i].style) {
    +            pos = frmEmitClamped(text, sizeof(text), pos, ", %s", sFontWords[i].word);
    +        }
    +    }
    +
    +    addPropRow(PRP_ROW_FONT, text);
    +}
    +
    +
     static void addPropRow(const char *name, const char *value) {
         arrput(sCellData, strdup(name));
         arrput(sCellData, strdup(value ? value : ""));
    @@ -230,7 +273,7 @@ static void ctlEditRow(int32_t row) {
         snprintf(prompt, sizeof(prompt), "%s:", key);
     
         if (k->kind == PrjCtlBoolE) {
    -        snprintf(newValue, sizeof(newValue), "%s", frmParseBool(curValue) ? "False" : "True");
    +        snprintf(newValue, sizeof(newValue), "%s", frmParseBool(curValue) ? FRM_TEXT_FALSE : FRM_TEXT_TRUE);
         } else if (k->kind == PrjCtlIntE) {
             int32_t n = atoi(curValue);
     
    @@ -305,6 +348,63 @@ static void ctlShowRows(void) {
     
     
     // Walk tree items recursively to find the one matching a control name.
    +// The Font row: the font dialog starting at the font the selected control
    +// (or the form) draws with, fixed-pitch families only for a control that
    +// edits text by columns.  OK stores all six Font properties, or removes
    +// them when the choice is the font it has without them (so a .frm keeps
    +// only fonts that differ).
    +static void editFont(void) {
    +    int32_t        count = (int32_t)arrlen(sDs->form->controls);
    +    FrmCtrlT      *ctrl  = (sDs->selectedIdx >= 0 && sDs->selectedIdx < count) ? sDs->form->controls[sDs->selectedIdx] : NULL;
    +    WidgetT       *w     = ctrl ? dsgnWidget(ctrl) : dsgnFormWidget(sDs);
    +    DvxFontChoiceT chosen;
    +    DvxFontChoiceT inherited;
    +
    +    if (!w) {
    +        return;
    +    }
    +
    +    uint32_t flags = (w->wclass && (w->wclass->flags & WCLASS_FIXED_FONT)) ? DVX_FONT_DLG_FIXED : 0;
    +
    +    basFontChoiceOf(wgtFont(w), &chosen);
    +
    +    if (!dvxFontDialog(sPrpCtx, PRP_ROW_FONT, flags, &chosen)) {
    +        return;
    +    }
    +
    +    // The font it has without Font properties of its own
    +    wgtSetFont(w, NULL, 0, 0);
    +    basFontChoiceOf(wgtFont(w), &inherited);
    +
    +    bool same = strcasecmp(chosen.family, inherited.family) == 0 && chosen.size == inherited.size && chosen.style == inherited.style;
    +
    +    for (int32_t i = 0; basFontPropAt(i); i++) {
    +        const char *name = basFontPropAt(i);
    +        char        text[FRM_MAX_TEXT];
    +
    +        basFontPropText(&chosen, name, text, sizeof(text));
    +
    +        if (!ctrl) {
    +            frmModelSetFormProp(sDs->form, name, same ? "" : text);
    +        } else if (same) {
    +            frmCtrlRemoveProp(ctrl, name);
    +        } else {
    +            frmModelSetCtrlProp(sDs->form, ctrl, name, text);
    +        }
    +    }
    +
    +    sDs->form->dirty = true;
    +    dsgnApplyFonts(sDs);
    +    dsgnFitFormWin(sDs);
    +
    +    if (sDs->formWin) {
    +        dvxInvalidateWindow(sPrpCtx, sDs->formWin);
    +    }
    +
    +    prpRefresh(sDs);
    +}
    +
    +
     static WidgetT *findTreeItemByName(WidgetT *parent, const char *name, int32_t index) {
         for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) {
             const char *label = (const char *)item->userData;
    @@ -473,16 +573,18 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam
     // the runtime form/control property tables for the rest.
     static uint8_t getPropType(const char *propName, const char *typeName) {
         // Designer rows with a fixed editor: the read-only Type/Index rows,
    -    // Name and Layout (editable at design time although the runtime
    -    // rejects assignment), and the data-binding pickers.
    +    // the Font row (the font dialog), Name and Layout (editable at design
    +    // time although the runtime rejects assignment), and the data-binding
    +    // pickers.
         static const struct {
             const char *name;
             uint8_t     kind;
         } fixedKinds[] = {
    -        { PRP_ROW_TYPE,   PRP_EDIT_READONLY   },
    -        { BAS_PROP_INDEX,  PRP_EDIT_READONLY   },
    -        { BAS_PROP_NAME,   WGT_IFACE_STRING    },
    -        { BAS_PROP_LAYOUT, PRP_EDIT_LAYOUT     },
    +        { PRP_ROW_TYPE,          PRP_EDIT_READONLY   },
    +        { PRP_ROW_FONT,          PRP_EDIT_FONT       },
    +        { BAS_PROP_INDEX,        PRP_EDIT_READONLY   },
    +        { BAS_PROP_NAME,         WGT_IFACE_STRING    },
    +        { BAS_PROP_LAYOUT,       PRP_EDIT_LAYOUT     },
             { BAS_PROP_DATASOURCE,   PRP_EDIT_DATASOURCE },
             { BAS_PROP_DATAFIELD,    PRP_EDIT_DATAFIELD  },
             { BAS_PROP_RECORDSOURCE, PRP_EDIT_RECORDSRC  },
    @@ -682,12 +784,17 @@ static void onPropDblClick(WidgetT *w) {
             return;
         }
     
    +    if (propType == PRP_EDIT_FONT) {
    +        editFont();
    +        return;
    +    }
    +
         char newValue[FRM_MAX_TEXT];
     
         if (propType == WGT_IFACE_BOOL) {
             // Toggle boolean on double-click -- no input box
             bool cur = frmParseBool(curValue);
    -        snprintf(newValue, sizeof(newValue), "%s", cur ? "False" : "True");
    +        snprintf(newValue, sizeof(newValue), "%s", cur ? FRM_TEXT_FALSE : FRM_TEXT_TRUE);
         } else if (propType == WGT_IFACE_ENUM) {
             // Enum: cycle to next value on double-click
             const WgtPropDescT *pd = frmIfaceProp(ctrlTypeName, propName);
    @@ -878,6 +985,8 @@ static void onPropDblClick(WidgetT *w) {
     
                 if (frmIntPropFind(propName)) {
                     dsgnSyncWidgetGeom(ctrl);
    +            } else if (basFontPropIs(propName)) {
    +                dsgnApplyFonts(sDs);
                 } else if (strcasecmp(propName, BAS_PROP_ENABLED) == 0 && !p) {
                     // Descendants keep their own Enabled; they are drawn
                     // disabled only while an ancestor is.  Visible is stored
    @@ -934,6 +1043,10 @@ static void onPropDblClick(WidgetT *w) {
                 if (isPad && sDs->contentBox) {
                     dsgnApplyGaps(sDs);
                 }
    +
    +            if (basFontPropIs(propName)) {
    +                dsgnApplyFonts(sDs);
    +            }
             }
     
             sDs->form->dirty = true;
    @@ -1389,14 +1502,15 @@ void prpRefresh(DsgnStateT *ds) {
             }
     
             if (!frmIfaceProp(ctrl->typeName, BAS_PROP_VISIBLE)) {
    -            addPropRow(BAS_PROP_VISIBLE, ctrl->visible ? "True" : "False");
    +            addPropRow(BAS_PROP_VISIBLE, ctrl->visible ? FRM_TEXT_TRUE : FRM_TEXT_FALSE);
             }
     
             if (!frmIfaceProp(ctrl->typeName, BAS_PROP_ENABLED)) {
    -            addPropRow(BAS_PROP_ENABLED, ctrl->enabled ? "True" : "False");
    +            addPropRow(BAS_PROP_ENABLED, ctrl->enabled ? FRM_TEXT_TRUE : FRM_TEXT_FALSE);
             }
     
             addPropRow(BAS_PROP_HELPTOPIC, ctrl->helpTopic);
    +        addFontRow(dsgnWidget(ctrl));
     
             for (int32_t i = 0; i < (int32_t)arrlen(ctrl->props); i++) {
                 addPropRow(ctrl->props[i].name, ctrl->props[i].value);
    @@ -1463,6 +1577,8 @@ void prpRefresh(DsgnStateT *ds) {
                     addPropRow(formProps[i].name, valBuf);
                 }
             }
    +
    +        addFontRow(dsgnFormWidget(ds));
         }
     
         wgtListViewSetData(sPropList, (const char **)sCellData, (int32_t)arrlen(sCellData) / PRP_CELL_COLUMNS);
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideToolbox.c b/src/apps/kpunch/dvxbasic/ide/ideToolbox.c
    index 483f442d..6d174812 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideToolbox.c
    +++ b/src/apps/kpunch/dvxbasic/ide/ideToolbox.c
    @@ -22,22 +22,25 @@
     
     // ideToolbox.c -- DVX BASIC form designer toolbox window
     //
    -// A small floating window with buttons for each registered widget
    -// type that has a basName (VB-style name), in alphabetical order of
    -// that name (basCtlTypeAt). Built dynamically from the widget interface
    -// registry: native widgets and installed BASIC controls (their .wgt
    -// file's icon24 / name resources), and the project's own UserControls
    -// the designer registered (their header's ToolboxIcon picture, a file
    -// in the project's folder, and Description); other programs' private
    -// controls are left out.
    +// A small floating window with buttons for each registered widget type
    +// that has a basName (VB-style name), in alphabetical order of that name
    +// (basCtlTypeAt), flowed in a WrapBox.  The window opens TBX_COLS tools
    +// across and as tall as the tools need (as far as the screen allows) and
    +// can be resized: the tools reflow to its width, and the window's scroll
    +// bar reaches any that do not fit.  Built dynamically from the widget
    +// interface registry: native widgets and installed BASIC controls (their
    +// .wgt file's icon24 / name resources), and the project's own
    +// UserControls the designer registered (their header's ToolboxIcon
    +// picture, a file in the project's folder, and Description); other
    +// programs' private controls are left out.
     
     #include "ideToolbox.h"
     #include "dvxApp.h"
     #include "dvxRes.h"
     #include "dvxWm.h"
    -#include "box/box.h"
     #include "button/button.h"
     #include "imageButton/imgBtn.h"
    +#include "wrapBox/wrapBox.h"
     
     #include 
     #include 
    @@ -48,8 +51,7 @@
     // Constants
     // ============================================================
     
    -#define TBX_COLS         4
    -#define TBX_WIN_W        120
    +#define TBX_WIN_W        120  // the window while its tools load (then fitted)
     #define TBX_WIN_H        250
     #define TBX_TOOLTIP_LEN  64   // tooltip text loaded from the .wgt "name" resource
     
    @@ -74,6 +76,7 @@ static bool           sBuilding = false; // tbxCreate is pumping events between
     static void     onTbxClose(WindowT *win);
     static void     onToolClick(WidgetT *w);
     static uint8_t *projectToolIcon(AppContextT *ctx, const WgtIfaceT *iface, char *tooltip, int32_t tooltipSize, int32_t *outW, int32_t *outH, int32_t *outPitch);
    +static void     tbxFitStart(AppContextT *ctx, WindowT *win, int32_t y, WidgetT *wrap, WidgetT **buttons);
     
     
     static void onTbxClose(WindowT *win) {
    @@ -130,7 +133,7 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
         sDs = ds;
         arrsetlen(sTbxTools, 0);
     
    -    WindowT *win = dvxCreateWindow(ctx, "Toolbox", 0, y, TBX_WIN_W, TBX_WIN_H, false);
    +    WindowT *win = dvxCreateWindow(ctx, "Toolbox", 0, y, TBX_WIN_W, TBX_WIN_H, true);
     
         if (!win) {
             return NULL;
    @@ -138,12 +141,15 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
     
         win->onClose = onTbxClose;
     
    -    WidgetT *root = wgtInitWindow(ctx, win);
    -
    -    int32_t   col     = 0;
    -    WidgetT  *row     = NULL;
    +    WidgetT  *root    = wgtInitWindow(ctx, win);
    +    WidgetT  *wrap    = root ? wgtWrapBox(root) : NULL;
         WidgetT **buttons = NULL; // stb_ds array, parallel to sTbxTools
     
    +    if (!wrap) {
    +        dvxDestroyWindow(ctx, win);
    +        return NULL;
    +    }
    +
         sBuilding = true;
     
         // The control types in alphabetical order (basCtlTypeAt), fetched by
    @@ -190,18 +196,12 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
             arrput(sTbxTools, entry);
             int32_t toolIdx = (int32_t)arrlen(sTbxTools) - 1;
     
    -        // Start a new row every TBX_COLS buttons
    -        if (col == 0) {
    -            row = wgtHBox(root);
    -        }
    -
    -        // Create button in the current row
             WidgetT *btn = NULL;
     
             if (iconData) {
    -            btn = wgtImageButton(row, iconData, iconW, iconH, iconPitch);
    +            btn = wgtImageButton(wrap, iconData, iconW, iconH, iconPitch);
             } else {
    -            btn = wgtButton(row, iface->basName);
    +            btn = wgtButton(wrap, iface->basName);
             }
     
             // Out of memory (wgtImageButton frees iconData itself): drop the tool.
    @@ -214,8 +214,6 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
             btn->userData = (void *)(intptr_t)toolIdx;
             arrput(buttons, btn);
     
    -        col = (col + 1) % TBX_COLS;
    -
             // Yield to keep the UI responsive while loading icons
             dvxUpdate(ctx);
         }
    @@ -229,9 +227,8 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
             wgtSetTooltip(buttons[i], sTbxTools[i].tooltip);
         }
     
    +    tbxFitStart(ctx, win, y, wrap, buttons);
         arrfree(buttons);
    -
    -    dvxFitWindow(ctx, win);
         return win;
     }
     
    @@ -245,3 +242,41 @@ void tbxDestroy(AppContextT *ctx, WindowT *win) {
         sTbxTools = NULL;
         sDs       = NULL;
     }
    +
    +
    +// The window's starting size: TBX_COLS of the widest tool across, the
    +// WrapBox laid out once at exactly that width so it knows the height its
    +// rows take, then the window fitted to it.  The WrapBox, not the window,
    +// is laid out at that width, and the tools are measured by a layout, not
    +// a fit: the frames that drew the tools while they loaded wrapped them
    +// at the loading size, which can leave the window a scroll bar narrower
    +// and its content far taller than the final rows.  The width hint goes
    +// again so the window can be made narrower (the tools reflow).  Too tall
    +// for the screen below y: cut at the screen's bottom and one window
    +// scroll bar wider, so TBX_COLS still fit beside the bar.
    +static void tbxFitStart(AppContextT *ctx, WindowT *win, int32_t y, WidgetT *wrap, WidgetT **buttons) {
    +    int32_t btnW = 0;
    +
    +    wgtLayout(win->widgetRoot, win->contentW, win->contentH, &ctx->font);
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(buttons); i++) {
    +        btnW = DVX_MAX(btnW, buttons[i]->calcMinW);
    +    }
    +
    +    int32_t wrapW = TBX_COLS * btnW + (TBX_COLS - 1) * wgtGetSpacingPx(wrap) + 2 * wgtGetPaddingPx(wrap);
    +
    +    wrap->minW = wgtPixels(wrapW);
    +    dvxFitWindowW(ctx, win);
    +    wgtLayout(wrap, wrapW, win->contentH, &ctx->font);
    +    dvxFitWindowH(ctx, win);
    +    wrap->minW = 0;
    +
    +    // dvxResizeWindow keeps a window on screen by moving it up: put it
    +    // back at y once it is short enough to fit there
    +    if (y + win->h > ctx->display.height) {
    +        dvxResizeWindow(ctx, win, win->w + SCROLLBAR_WIDTH, ctx->display.height - y);
    +        win->y = y;
    +    }
    +
    +    wgtInvalidate(win->widgetRoot);
    +}
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideToolbox.h b/src/apps/kpunch/dvxbasic/ide/ideToolbox.h
    index 70bb2e68..23b08cb4 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideToolbox.h
    +++ b/src/apps/kpunch/dvxbasic/ide/ideToolbox.h
    @@ -27,11 +27,16 @@
     
     #include "ideDesigner.h"
     
    -// Create the toolbox floating window at the given y coordinate.
    -// Caller owns y so the window is painted at its final location
    -// during the asynchronous icon-loading dvxUpdate cycles; moving
    -// the window after creation would leave stale paint at the old
    -// position on the backbuffer.  Returns the WindowT pointer.
    +// Tools across when the window opens (it can be resized: they reflow)
    +#define TBX_COLS 4
    +
    +// Create the toolbox floating window at the given y coordinate: its
    +// tools in a WrapBox (the window root's first child), TBX_COLS across,
    +// the window as tall as they need down to the screen's bottom (a scroll
    +// bar below that).  Caller owns y so the window is painted at its final
    +// location during the asynchronous icon-loading dvxUpdate cycles; moving
    +// the window after creation would leave stale paint at the old position
    +// on the backbuffer.  Returns the WindowT pointer.
     WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y);
     
     // Destroy the toolbox window.
    diff --git a/src/apps/kpunch/dvxbasic/ideguide.dhs b/src/apps/kpunch/dvxbasic/ideguide.dhs
    index 6ad9becd..1cbb7f0e 100644
    --- a/src/apps/kpunch/dvxbasic/ideguide.dhs
    +++ b/src/apps/kpunch/dvxbasic/ideguide.dhs
    @@ -109,13 +109,13 @@ The IDE compiles BASIC source into bytecode and runs it in an integrated virtual
       ---
       Project Properties...             Edit project metadata (name, author, version, startup form, icon, etc.) and the command line arguments used when the program is run from the IDE.
       ---
    -  Add File...            Ctrl+O     Add a .bas, .frm, .cls or .ctl file to the project. If no project is open, creates an implicit project from the file. A file that cannot be read, or is 64 KB or larger, is refused with a message.
    +  Add File...            Ctrl+O     Add a .bas, .frm, .cls, or .ctl file to the project. If no project is open, creates an implicit project from the file. A file that cannot be read, or is 64 KB or larger, is refused with a message.
       New Class Module                  Add a new, empty class module (Class1.cls, Class2.cls, ...) to the project and open it in the Code Editor. See Class Modules under Project System.
       New UserControl                   Add a new UserControl, a control of your own (UserControl1.ctl, UserControl2.ctl, ...), to the project and open it in the Code Editor. See UserControls in the IDE.
       Save File              Ctrl+S     Save the active file to disk.
       Save All                          Save all modified files in the project.
       ---
    -  Make Executable...                Compile the project and save it as a standalone .app file. Prompts for a debug or release build. A release build leaves out debugging information and gives forms and controls short internal names, except in a program that uses names as text (the Name property, Controls, DesignSelection or a DesignMouse event procedure), which keeps them.
    +  Make Executable...                Compile the project and save it as a standalone .app file. Prompts for a debug or release build. A release build leaves out debugging information and gives forms and controls short internal names, except in a program that uses names as text (the Name property, Controls, DesignSelection, or a DesignMouse event procedure), which keeps them.
       Make Control...                   In a control project (Project Properties, Type Control) this item replaces Make Executable: it compiles the project and saves its controls as a control file (.wgt, named after the project). See UserControls in the IDE.
       ---
       Rename File...                    Give the file selected in the Project Explorer a new name, on disk and in the project. See Renaming a File under Project System.
    @@ -162,7 +162,7 @@ Files are saved exactly as edited (LF line endings). Each save writes a temporar
       Replace...   Ctrl+H     Open the Find/Replace dialog with replace enabled.
     .endtable
     
    -Cut, Copy, Paste, Select All and Delete act on the window you last worked in: the form designer's selected control, or the text of the code, output or immediate window.
    +Cut, Copy, Paste, Select All, and Delete act on the window you last worked in: the form designer's selected control, or the text of the code, output, or immediate window.
     
     .link ide.menu.run Run Menu
     .link ide.overview Back to Overview
    @@ -191,7 +191,7 @@ Cut, Copy, Paste, Select All and Delete act on the window you last worked in: th
       Run                      F5               Compile and run the program. If paused at a breakpoint, resumes execution with debugging disabled (runs free).
       Debug                    Shift+F5         Compile and run with the debugger active. Breakpoints are active but execution does not pause at the first statement. If paused, resumes to the next breakpoint.
       Run Without Recompile    Ctrl+F5          Re-run the last compiled module without recompiling.
    -  Stop                     Esc              Stop the running program. Run, Debug and the project commands stay disabled until the program has finished shutting down.
    +  Stop                     Esc              Stop the running program. Run, Debug, and the project commands stay disabled until the program has finished shutting down.
       ---
       Step Into                F8               Execute one statement, stepping into SUB/FUNCTION calls. If idle, starts a debug session and breaks at the first statement.
       Step Over                Shift+F8         Execute one statement, stepping over SUB/FUNCTION calls.
    @@ -375,7 +375,7 @@ At the top of the Code Editor are two dropdown lists:
     .item Function -- lists all event handlers (procedures) for the selected object. Implemented handlers are listed first (plain text); unimplemented handlers follow in brackets (e.g., [Click]). Selecting an unimplemented event creates a new event handler stub. Only the events the object can get are listed: Paint, for example, appears for the form and for PictureBoxes, the objects a program draws on.
     .endlist
     
    -A WithEvents variable (Dim WithEvents acct As Account, also Private or Public, in a module, a form or a class module) is listed as an object; its events are the ones its class module declares with Event. Picking one of them creates the handler acct_EventName with the event's own parameters, so it matches what the class raises. Selecting the variable when it has no handler yet creates the handler for the class's first event, as a control's default event is created.
    +A WithEvents variable (Dim WithEvents acct As Account, also Private or Public, in a module, a form, or a class module) is listed as an object; its events are the ones its class module declares with Event. Picking one of them creates the handler acct_EventName with the event's own parameters, so it matches what the class raises. Selecting the variable when it has no handler yet creates the handler for the class's first event, as a control's default event is created.
     
     The editor shows one procedure at a time. Each procedure has its own buffer, and switching between them is instantaneous. The (General) section contains module-level declarations and code.
     
    @@ -397,7 +397,7 @@ The editor applies real-time syntax coloring as you type. Seven categories are h
       Operators         =, <, >, +, -, *, /, \, &
     .endtable
     
    -Date literals are colored as numbers only when they are real dates, as the compiler reads them; the # of a file number (CLOSE #1) is not. Some words are colored like keywords without being reserved -- built-in type names, Empty, Null, EACH, EVENT, WITHEVENTS, and the console and graphics statements such as CLS, LOCATE, COLOR, SCREEN, PSET, CIRCLE, PAINT, PCOPY and PMAP. A program may use them as its own names, and after a dot (Text1.Width, Picture1.Point) they are shown as ordinary names.
    +Date literals are colored as numbers only when they are real dates, as the compiler reads them; the # of a file number (CLOSE #1) is not. Some words are colored like keywords without being reserved -- built-in type names, Empty, Null, EACH, EVENT, WITHEVENTS, and the console and graphics statements such as CLS, LOCATE, COLOR, SCREEN, PSET, CIRCLE, PAINT, PCOPY, and PMAP. A program may use them as its own names, and after a dot (Text1.Width, Picture1.Point) they are shown as ordinary names.
     
     .h2 Editor Features
     
    @@ -432,20 +432,20 @@ The Form Designer provides a visual design surface for editing .frm files. Switc
     .item Selection -- click a control to select it. The selected control is highlighted with grab handles.
     .item Grab handles -- 6x6 pixel handles appear on the right edge (E), bottom edge (S), and bottom-right corner (SE) of the selected control. Drag a handle to resize the control.
     .item Reordering -- drag a control to reorder it among its siblings in the layout (VBox/HBox). A container moves together with its contents.
    -.item Placing controls -- select a control type in the Toolbox, then click on the form to place a new instance. The control is auto-named (e.g., Command1, Command2), skipping names already used by a control, a menu item or the form. Clicking the same tool again deselects it (toggles back to pointer mode). Controls nest at most 15 levels deep; a drop into a container that is already that deep goes into its parent container.
    +.item Placing controls -- select a control type in the Toolbox, then click on the form to place a new instance. The control is auto-named (e.g., Command1, Command2), skipping names already used by a control, a menu item, or the form. Clicking the same tool again deselects it (toggles back to pointer mode). Controls nest at most 15 levels deep; a drop into a container that is already that deep goes into its parent container.
     .item Menu bar preview -- if the form has menu items (defined via the Menu Editor), a preview menu bar is rendered on the design window.
     .item Delete key -- removes the selected control and everything inside it.
     .endlist
     
    -Opening a form in the designer does not mark it modified. A control whose Visible property is False is still shown on the design surface (the setting applies when the program runs). Disabling a container greys the controls inside it; a control's own Enabled = False is kept when the container is enabled again. A .frm file without an AutoSize line opens without AutoSize; AutoSize = True wins over Width and Height lines. Text values (Caption, Text and so on) are limited to 379 characters.
    +Opening a form in the designer does not mark it modified. A control whose Visible property is False is still shown on the design surface (the setting applies when the program runs). Disabling a container greys the controls inside it; a control's own Enabled = False is kept when the container is enabled again. A .frm file without an AutoSize line opens without AutoSize; AutoSize = True wins over Width and Height lines. Text values (Caption, Text, and so on) are limited to 379 characters.
     
     .h2 Menu Editor
     
    -The Menu Editor (View > Menu Editor, Ctrl+E) edits the active form's menu bar and popup menus. Menus nest at most 15 levels (0-14); indenting refuses when any item below would go deeper. OK rejects an item nested more than one level below the item above it, and a blank item that has sub-items (it must be named); blank items without sub-items are dropped. The window's close box cancels. Shortcut is a key that runs the item (F2, Ctrl+X, Alt+Enter ...), shown at its right; Text is key text shown without making a shortcut (see Menu Item Properties); OK rejects a Shortcut it does not know. Renaming a menu item renames its event handlers in the code and updates the ContextMenu setting of the form and its controls; inserting, deleting and moving items does not rename anything.
    +The Menu Editor (View > Menu Editor, Ctrl+E) edits the active form's menu bar and popup menus. Menus nest at most 15 levels (0-14); indenting refuses when any item below would go deeper. OK rejects an item nested more than one level below the item above it, and a blank item that has sub-items (it must be named); blank items without sub-items are dropped. The window's close box cancels. Shortcut is a key that runs the item (F2, Ctrl+X, Alt+Enter ...), shown at its right; Text is key text shown without making a shortcut (see Menu Item Properties); OK rejects a Shortcut it does not know. Renaming a menu item renames its event handlers in the code and updates the ContextMenu setting of the form and its controls; inserting, deleting, and moving items does not rename anything.
     
     .h2 Form Properties
     
    -Forms have the following design-time properties: Name, Caption, Width, Height, Left, Top, Layout (VBox or HBox), Centered, AutoSize, Resizable, HelpTopic and ContextMenu.
    +Forms have the following design-time properties: Name, Caption, Width, Height, Left, Top, Layout (VBox or HBox), Centered, AutoSize, Resizable, HelpTopic, ContextMenu, and the font (the Font row; see Properties Panel).
     
     .link ide.properties Properties Panel
     .link ide.toolbox Toolbox
    @@ -464,6 +464,8 @@ Forms have the following design-time properties: Name, Caption, Width, Height, L
     .index Classes Section
     .index Controls Section
     .index Project Type
    +.index Fonts Section
    +.index Project Fonts
     
     .h1 Project System
     
    @@ -479,7 +481,8 @@ A DVX BASIC project is stored as a .dbp file (DVX BASIC Project). The project fi
     .item More Than One Copy -- "Allow more than one copy to run at once" ([Settings] MultiInstance = True): the compiled program can be started again while it is running, and each copy runs on its own (like the Clock and the File Manager). Without it, starting the program again only says that it is already running.
     .item Help File -- a compiled help file (.hlp) for the program. It is copied next to the compiled program, and F1 in the running program opens it, at the HelpTopic of the focused control or, failing that, of the form. Add help.bas to the project and call HelpView to open it from a Help menu.
     .item Type -- what the project makes: a program (an .app, the default) or, with Type = Control, a control file (a .wgt holding the project's UserControls for every program). See UserControls in the IDE.
    -.item File list -- relative paths of all the project's files: modules (.bas) in the [Modules] section, class modules (.cls) in the [Classes] section, UserControls (.ctl) in the [Controls] section and forms (.frm) in the [Forms] section.
    +.item File list -- relative paths of all the project's files: modules (.bas) in the [Modules] section, class modules (.cls) in the [Classes] section, UserControls (.ctl) in the [Controls] section, and forms (.frm) in the [Forms] section.
    +.item Fonts -- the .DFN fonts the program carries, in the [Fonts] section (see Fonts a Program Carries).
     .endlist
     
     The file sections all have the same shape: one FileN line per file, numbered from 0, and a Count line:
    @@ -499,9 +502,15 @@ File0 = form1.frm
     Count = 1
     .endcode
     
    +.h2 Fonts a Program Carries
    +
    +A program can carry fonts of its own, so its text looks the same whatever fonts were installed. In Project > Properties, Add... in the Fonts row picks a .DFN font (one outside the project's directory can be copied into it); Remove takes the chosen one off the list (the file stays). The fonts are listed in the [Fonts] section of the project file. Make builds them into the program as font resources, and the program has them whenever it runs; running it from the IDE gives it the same fonts until it stops. A program carries at most 64 fonts.
    +
    +A program's fonts are its own: other programs never see them. Where a font it carries is of a family that is installed too, its sizes and styles come first and the installed family fills in the rest. Fonts are made with the SDK's mkfont from TrueType, Type 1, and bitmap fonts -- one size and only the characters the program uses keeps a font small -- or copied from DVX's FONTS folder. Each .DFN carries the notice and licence of the font it was made from.
    +
     .h2 How Projects are Compiled
     
    -When the project is compiled, all source files are concatenated into a single source stream: the modules first, then the class modules, then the UserControls, then the forms. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas, .cls or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form, and the code of each class module and UserControl is likewise marked with its class name; the header lines of a class module or UserControl are not compiled.
    +When the project is compiled, all source files are concatenated into a single source stream: the modules first, then the class modules, then the UserControls, then the forms. The IDE tracks which lines belong to which file so error messages and debugger locations point to the correct .bas, .cls, or .frm file. The code section of each .frm file is preceded by a hidden BEGINFORM marker that ties its code to its form, and the code of each class module and UserControl is likewise marked with its class name; the header lines of a class module or UserControl are not compiled.
     
     .h2 Compile-Time Validation
     
    @@ -511,7 +520,7 @@ Before the source is handed to the compiler, the IDE scans every .frm file and c
     .item Unknown widget types -- any Begin TypeName ... End block whose TypeName does not match a registered widget is flagged. The error message names the offending form, control, and type, and hints that DVX uses VB6-style names (SpinButton, not Spinner).
     .item Unknown properties -- statements of the form CtrlName.Property = value, or reads of CtrlName.Property, are checked against the widget's declared property list. A typo such as btn.Captoin triggers a compile error with the form, control, and property name. A control name that is used with different types on different forms is not checked.
     .item Unknown methods -- method calls CtrlName.Method ... are checked the same way against the widget's method list, catching mistakes like list.AddTiem before the program ever runs.
    -.item Menu items -- only Name (read), Checked and Enabled can be used on a menu item in code; Caption, Visible, RadioCheck, Shortcut and ShortcutText are design-time settings.
    +.item Menu items -- only Name (read), Checked, and Enabled can be used on a menu item in code; Caption, Visible, RadioCheck, Shortcut, and ShortcutText are design-time settings.
     .endlist
     
     These checks use the same metadata the Properties panel and Toolbox read from each installed control, so property and method names always match the current widget set without any separate list to maintain.
    @@ -525,27 +534,27 @@ These checks use the same metadata the Properties panel and Toolbox read from ea
       Open Project          Opens a .dbp file and loads all referenced files into memory. A project file that is 64 KB or larger, or cannot be read, is not loaded; opening it later reports "Cannot load : unreadable or larger than 65535 bytes." Carriage returns are removed when files are loaded.
       Save Project          Writes the .dbp file to disk.
       Close Project         Closes the project, prompting to save unsaved changes.
    -  Add File              Adds a .bas, .frm, .cls or .ctl file to the project. Opening a file without a project auto-creates an implicit project.
    +  Add File              Adds a .bas, .frm, .cls, or .ctl file to the project. Opening a file without a project auto-creates an implicit project.
       New Class Module      Adds a new class module to the project and opens it (see Class Modules below).
       New UserControl       Adds a new UserControl to the project and opens it (see UserControls in the IDE).
       Rename File           Gives a file a new name on disk and in the project file (see Renaming a File below).
       Remove File           Removes a file from the project (prompts to save if modified). Removing the startup form clears the Startup Form setting.
    -  Project Properties    Opens a dialog for editing project metadata (name, type, author, company, version, copyright, description, icon, help file, startup form), whether more than one copy may run at once, and the Arguments for runs from the IDE.
    +  Project Properties    Opens a dialog for editing project metadata (name, type, author, company, version, copyright, description, icon, help file, startup form), the fonts the program carries, whether more than one copy may run at once, and the Arguments for runs from the IDE.
     .endtable
     
     .h2 Renaming a File
     
    -Select a file in the Project Explorer and choose File > Rename File. Type the new file name; the extension may be left out (a module stays .bas, a class module .cls, a UserControl .ctl and a form .frm, and cannot be given another kind's extension). The file is renamed in the project's directory and the project file is saved at once with the new name, so the two always agree. Nothing changes, and a message says why, when the name is not a valid file name, when another project file or any file in the directory already has it, or when the file or the project file cannot be written -- an existing file is never overwritten. A file that has never been saved is simply given the new name, and is written under it when you save.
    +Select a file in the Project Explorer and choose File > Rename File. Type the new file name; the extension may be left out (a module stays .bas, a class module .cls, a UserControl .ctl, and a form .frm, and cannot be given another kind's extension). The file is renamed in the project's directory and the project file is saved at once with the new name, so the two always agree. Nothing changes, and a message says why, when the name is not a valid file name, when another project file or any file in the directory already has it, or when the file or the project file cannot be written -- an existing file is never overwritten. A file that has never been saved is simply given the new name, and is written under it when you save.
     
    -A class module named after its file (it has no Attribute VB_Name line, or one that matches the file name, as File > New Class Module writes it) takes the new name as its class name: its Attribute VB_Name line is changed to match (an edit that is saved with the file, like any other). The new name must then be a valid name that no other form, class module or UserControl uses. Code that uses the old class name is not changed. A UserControl named after its file (as File > New UserControl makes it) takes the new name too, on its Begin UserControl line; forms that use the old name are not changed. A form keeps its Name property; rename the form itself in the Properties window.
    +A class module named after its file (it has no Attribute VB_Name line, or one that matches the file name, as File > New Class Module writes it) takes the new name as its class name: its Attribute VB_Name line is changed to match (an edit that is saved with the file, like any other). The new name must then be a valid name that no other form, class module, or UserControl uses. Code that uses the old class name is not changed. A UserControl named after its file (as File > New UserControl makes it) takes the new name too, on its Begin UserControl line; forms that use the old name are not changed. A form keeps its Name property; rename the form itself in the Properties window.
     
     .h2 Project Explorer
     
    -The Project Explorer is a tree view window listing all project files, in four groups: Forms, Modules, Classes and Controls (UserControls). Double-click a file to open it: .bas, .cls and .ctl files open in Code view, .frm files open in Design view. The selected file in the Project Explorer determines the target for View > Code and View > Designer commands.
    +The Project Explorer is a tree view window listing all project files, in four groups: Forms, Modules, Classes, and Controls (UserControls). Double-click a file to open it: .bas, .cls, and .ctl files open in Code view, .frm files open in Design view. The selected file in the Project Explorer determines the target for View > Code and View > Designer commands.
     
     .h2 Class Modules
     
    -A class module (.cls file) defines a kind of object for your program: its member variables, properties and methods. The BASIC Language Reference describes how to write one (see Objects and Class Modules); this section covers working with them in the IDE.
    +A class module (.cls file) defines a kind of object for your program: its member variables, properties, and methods. The BASIC Language Reference describes how to write one (see Objects and Class Modules); this section covers working with them in the IDE.
     
     .list
     .item Create -- choose File > New Class Module. The IDE adds a new class module named Class1 (Class2, and so on, when that name is taken) to the Classes group, saved in the project's directory as Class1.cls, and opens it in the Code Editor. The new file holds only its header and Option Explicit; it is written to disk when you save it.
    @@ -565,7 +574,7 @@ Attribute VB_Name = "Class1"
     Option Explicit
     .endcode
     
    -Leave the VERSION and Attribute lines at the top of the file; they are not compiled as code. Each class name must be different from the other class names, the UserControl names and the form names in the project.
    +Leave the VERSION and Attribute lines at the top of the file; they are not compiled as code. Each class name must be different from the other class names, the UserControl names, and the form names in the project.
     
     .link ide.usercontrols UserControls in the IDE
     .link ide.overview Back to Overview
    @@ -603,11 +612,11 @@ Private Sub UserControl_Paint ()
     End Sub
     .endcode
     
    -The lines up to End are the control's header; the code follows. To use a .ctl file you already have, choose File > Add File and pick it (the UserControls file type). Open, save, rename and remove .ctl files as you do class modules: a UserControl named after its file takes the new name when the file is renamed.
    +The lines up to End are the control's header; the code follows. To use a .ctl file you already have, choose File > Add File and pick it (the UserControls file type). Open, save, rename, and remove .ctl files as you do class modules: a UserControl named after its file takes the new name when the file is renamed.
     
     .h2 Writing the Code
     
    -In the Code Editor, the Object list of a UserControl holds UserControl (the control itself) beside (General). Its Function list has the events the control gets: Initialize, Terminate, Resize, Paint, Click, DblClick, MouseDown, MouseMove, MouseUp, MouseWheel, KeyDown, KeyPress, KeyUp, GotFocus and LostFocus. Picking one that has no code yet writes its procedure with the right parameters, for example:
    +In the Code Editor, the Object list of a UserControl holds UserControl (the control itself) beside (General). Its Function list has the events the control gets: Initialize, Terminate, Resize, Paint, Click, DblClick, MouseDown, MouseMove, MouseUp, MouseWheel, KeyDown, KeyPress, KeyUp, GotFocus, and LostFocus. Picking one that has no code yet writes its procedure with the right parameters, for example:
     
     .code
     Private Sub UserControl_MouseDown (Button As Integer, Shift As Integer, X As Single, Y As Single)
    @@ -619,12 +628,12 @@ These procedures are Private: a Public one would be a method of the control.
     
     .h2 The Header in the Properties Window
     
    -While a UserControl is the active file, the Properties window shows its header instead of a form's controls: Name, Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description and DefaultEvent, each with the value the control takes (its default when the header leaves it out). Double-click a row to change it:
    +While a UserControl is the active file, the Properties window shows its header instead of a form's controls: Name, Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, and DefaultEvent, each with the value the control takes (its default when the header leaves it out). Double-click a row to change it:
     
     .table
       Row            How it is changed
       ---            -----------------
    -  Name           Type a new name. It must be a valid name that no form, class module or other UserControl of the project uses. Forms that use the old name are not changed.
    +  Name           Type a new name. It must be a valid name that no form, class module, or other UserControl of the project uses. Forms that use the old name are not changed.
       Width, Height  Enter a whole number of pixels (at least 1): the control's natural size.
       CanGetFocus    Each double-click switches between True and False.
       BackColor      Pick a color in the color dialog.
    @@ -637,15 +646,15 @@ The change is written into the header lines of the file (the rest of the file st
     
     .h2 Trying It on a Form
     
    -While the project is open, its UserControls are in the Toolbox like the controls DVX comes with: with the picture of their ToolboxIcon (or their name on the button), and their Description as the tooltip. Place one on a form as any other control. The control on the design surface is a real one: its code runs, with UserControl.UserMode False, so Initialize, Resize and Paint draw it as it will look, while your clicks and keys go to the designer.
    +While the project is open, its UserControls are in the Toolbox like the controls DVX comes with: with the picture of their ToolboxIcon (or their name on the button), and their Description as the tooltip. Place one on a form as any other control. The control on the design surface is a real one: its code runs, with UserControl.UserMode False, so Initialize, Resize, and Paint draw it as it will look, while your clicks and keys go to the designer.
     
     .list
     .item The Properties window lists the control's properties that can be set (its Public variables and Property Let procedures), with the values its code gives them. A value you change there is passed to its Property Let and saved in the .frm; when the control's code calls UserControl.PropertyChanged, the window shows the new values.
    -.item The Code Editor's Function list for the placed control has the events the control raises, then the events a form gets for any of its controls (GotFocus, LostFocus, Validate, DragDrop and DragOver); picking one writes its handler with the event's parameters (Sub Gauge1_Changed (ByVal Level As Integer)).
    +.item The Code Editor's Function list for the placed control has the events the control raises, then the events a form gets for any of its controls (GotFocus, LostFocus, Validate, DragDrop, and DragOver); picking one writes its handler with the event's parameters (Sub Gauge1_Changed (ByVal Level As Integer)).
     .item Double-clicking the control on the design surface opens its DefaultEvent's handler.
     .endlist
     
    -The designer compiles the project's UserControls (with its modules and class modules, but not its forms) when the project opens and again when a .ctl file is saved, added, removed or renamed, or its header is changed in the Properties window; the controls on the design surface are made again then. While the controls have a compile error the designer cannot show them (the status line says so); compile with F5 to see the error. The code of the project's modules (outside their procedures) does not run in the designer, so module variables start empty there. A UserControl whose name is the name of a control installed for every program cannot be shown in the designer, and the program stops when it is run ("Control cannot be registered"): give it another name.
    +The designer compiles the project's UserControls (with its modules and class modules, but not its forms) when the project opens and again when a .ctl file is saved, added, removed, or renamed, or its header is changed in the Properties window; the controls on the design surface are made again then. While the controls have a compile error the designer cannot show them (the status line says so); compile with F5 to see the error. The code of the project's modules (outside their procedures) does not run in the designer, so module variables start empty there. A UserControl whose name is the name of a control installed for every program cannot be shown in the designer, and the program stops when it is run ("Control cannot be registered"): give it another name.
     
     When the program runs, its UserControls are its own: they are registered again for the program, separately from the copies in the designer.
     
    @@ -668,6 +677,7 @@ A program project can hold UserControls too (list them under Controls): the prog
     .index Control Properties
     .index Property List
     .index Control Tree
    +.index Font row
     
     .h1 Properties Panel
     
    @@ -680,7 +690,9 @@ The Properties panel (Window > Properties) has two sections:
     
     Each control type exposes different properties (e.g., Caption, Text, Width, Height, MaxWidth, MaxHeight, Weight, Alignment, Enabled, Visible, and type-specific properties like DataSource and DataField for data-bound controls). A UserControl placed on a form lists the properties its code defines.
     
    -While a UserControl (.ctl) is the active file, the panel shows the control's header instead: Name, Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description and DefaultEvent (see UserControls in the IDE).
    +The Font row shows the font the selected control, or the form, draws with: its family, size in pixels, and styles, such as "DejaVu Sans, 14, Bold". Double-click it to choose a font in the Font dialog; the design surface shows it at once. The choice is kept as the six Font properties (FontName, FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru), which then appear in the list and can be changed one at a time too. Choosing the font the control would have anyway (its container's or the form's) removes them, so the control follows the form's font again. A font chosen for the form is used by every control that has none of its own. For a TextBox, TextArea, ComboBox, SpinButton, or Terminal the dialog lists only fixed-pitch families.
    +
    +While a UserControl (.ctl) is the active file, the panel shows the control's header instead: Name, Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, and DefaultEvent (see UserControls in the IDE).
     
     .link ide.designer Form Designer
     .link ide.usercontrols UserControls in the IDE
    @@ -697,6 +709,8 @@ While a UserControl (.ctl) is the active file, the panel shows the control's hea
     
     The Toolbox (Window > Toolbox) is a floating palette of buttons, one for each available control type. Every widget registered with DVX that has a BASIC name appears in the toolbox: the controls DVX comes with, the controls written in BASIC that are installed for every program (control files in the WIDGETS folder), and the open project's own UserControls (see UserControls in the IDE). The buttons are in alphabetical order of the control's name. A button shows the control's picture, or its name when it has none; its tooltip is the control's description.
     
    +The Toolbox opens four buttons across and as tall as its buttons need, down to the bottom of the screen. Resize it like any window: the buttons rearrange to fill its width, and when they do not all fit, a scroll bar reaches the rest.
    +
     Click a tool to select it (the active tool name is stored in the designer state), then click on the form to place a new instance. Click the same tool again to deselect it and return to pointer mode.
     
     .h2 Available Controls
    @@ -721,6 +735,8 @@ Click a tool to select it (the active tool name is stored in the designer state)
       OptionButton    Radio button (mutually exclusive within a group).
       PictureBox      Canvas for drawing and images.
       ProgressBar     Progress indicator bar.
    +  RichEdit        Text box for text in many fonts, styles, and colors.
    +  RichLabel       Text in many fonts, styles, and colors, in aligned paragraphs.
       ScrollPane      Scrollable container for large content.
       Spacer          Invisible spacing element for layout.
       SpinButton      Numeric up/down spinner.
    @@ -805,7 +821,7 @@ Each breakpoint records the file it belongs to, the line number within that file
     
     .h3 Breakpoint Adjustment on Edit
     
    -Breakpoints follow their statements when lines are added, removed, joined, split or pasted. A breakpoint whose line is deleted is removed.
    +Breakpoints follow their statements when lines are added, removed, joined, split, or pasted. A breakpoint whose line is deleted is removed.
     
     .h2 Stepping
     
    @@ -1039,7 +1055,7 @@ If the assignment target cannot be resolved (unknown variable, out-of-bounds ind
     The Output window is a read-only TextArea at the bottom-left of the screen. It displays:
     
     .list
    -.item PRINT output -- PRINT statement output from the running program is appended here, until the program uses a console or graphics statement (CLS, LOCATE, COLOR, SCREEN, LINE, INKEY$ and the others): from then on PRINT and INPUT use the program's own screen window instead (see Console and Graphics in the language reference).
    +.item PRINT output -- PRINT statement output from the running program is appended here, until the program uses a console or graphics statement (CLS, LOCATE, COLOR, SCREEN, LINE, INKEY$, and the others): from then on PRINT and INPUT use the program's own screen window instead (see Console and Graphics in the language reference).
     .item Runtime errors -- if the VM encounters a runtime error (division by zero, out-of-bounds, etc.), the error message and line number are displayed in the output with an Error on line N: prefix.
     .item Compile errors -- if compilation fails, the error message and location are shown.
     .endlist
    @@ -1083,7 +1099,7 @@ Open with Ctrl+F (Find) or Ctrl+H (Replace). The Find/Replace dialog is modeless
     .table
       Button        Action
       ------        ------
    -  Find Next     Find the next occurrence. Function scope searches the current procedure, Object scope the selected object's procedures, File scope the current file and Project scope every file. At the end of the current procedure the search wraps around to its start.
    +  Find Next     Find the next occurrence. Function scope searches the current procedure, Object scope the selected object's procedures, File scope the current file, and Project scope every file. At the end of the current procedure the search wraps around to its start.
       Replace       Replace the current match and find the next one.
       Replace All   Replace all occurrences within the selected scope.
       Close         Close the dialog.
    @@ -1136,7 +1152,7 @@ These fields set the default values for new project metadata:
     
     .h2 Colors Tab
     
    -Customizes the syntax-highlighting colors used in the code editor. The left side is a list of seven color entries (Default Text, Keywords, Strings, Comments, Numbers, Operators, Types). Select an entry to see its color, and its red, green and blue amounts, on the right. Click Change... (or double-click the entry) to pick a new color in the color box: click a basic color or mix one with the Red, Green and Blue sliders or number boxes, then click OK. Changes are applied to the open editor when you click OK in the Preferences dialog.
    +Customizes the syntax-highlighting colors used in the code editor. The left side is a list of seven color entries (Default Text, Keywords, Strings, Comments, Numbers, Operators, Types). Select an entry to see its color, and its red, green, and blue amounts, on the right. Click Change... (or double-click the entry) to pick a new color in the color box: click a basic color or mix one with the Red, Green, and Blue sliders or number boxes, then click OK. Changes are applied to the open editor when you click OK in the Preferences dialog.
     
     .link ide.overview Back to Overview
     
    @@ -1190,17 +1206,17 @@ Customizes the syntax-highlighting colors used in the code editor. The left side
     
     .h1 Changes in This Release
     
    -This release fixes a large number of problems in the language, the runtime, the controls and the IDE. Most programs keep working unchanged. Three changes need attention in existing programs: variable scope (see Module Variables Need SHARED), the order of the bytes in a color, and text stored in numbers (see Colors and Numbers Follow Visual Basic).
    +This release fixes a large number of problems in the language, the runtime, the controls, and the IDE. Most programs keep working unchanged. Three changes need attention in existing programs: variable scope (see Module Variables Need SHARED), the order of the bytes in a color, and text stored in numbers (see Colors and Numbers Follow Visual Basic).
     
     .h2 Module Variables Need SHARED
     
     DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable declared at module level (including the code section of a form) only when it is declared with DIM SHARED, or named in a SHARED statement inside the procedure. Before, every module variable was visible everywhere.
     
     .list
    -.item In an existing program, change DIM to DIM SHARED for every module-level variable that a SUB, FUNCTION or event handler uses.
    +.item In an existing program, change DIM to DIM SHARED for every module-level variable that a SUB, FUNCTION, or event handler uses.
     .item Without that change such a name silently becomes a local variable of the procedure (or a compile error with OPTION EXPLICIT, or when the procedure indexes or dots it).
     .item New: the SHARED statement inside a procedure, and REDIM SHARED at module level. DIM SHARED inside a procedure is a compile error.
    -.item CONSTs, TYPEs, procedures, forms and controls are visible everywhere as before; a DEF FN still sees every module variable.
    +.item CONSTs, TYPEs, procedures, forms, and controls are visible everywhere as before; a DEF FN still sees every module variable.
     .endlist
     
     .link lang.declarations See: Variable Scope and SHARED
    @@ -1208,8 +1224,8 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec
     .h2 Colors and Numbers Follow Visual Basic
     
     .list
    -.item A color is &HBBGGRR, as in Visual Basic: RGB(255, 0, 0) is &HFF (red) and &HFF0000 is blue. RGB, GETRED / GETGREEN / GETBLUE, ForeColor, BackColor, FillColor, the drawing methods, POINT, the Canvas control and DlgColor all use this form. A program that wrote colors as &HRRGGBB numbers must swap the red and blue bytes, or build its colors with RGB. New: QBCOLOR(n) and the constants vbBlack, vbRed, vbGreen, vbYellow, vbBlue, vbMagenta, vbCyan and vbWhite.
    -.item Text used as a number must be a number, as in Visual Basic: n = "12" stores 12, but n = "abc", "abc" + 2 or a READ of DATA abc into a number raise error 13 (Type mismatch) and leave the variable as it was. VAL still reads the number at the start of any text, and INPUT reads numbers the way VAL does.
    +.item A color is &HBBGGRR, as in Visual Basic: RGB(255, 0, 0) is &HFF (red) and &HFF0000 is blue. RGB, GETRED / GETGREEN / GETBLUE, ForeColor, BackColor, FillColor, the drawing methods, POINT, the Canvas control, and DlgColor all use this form. A program that wrote colors as &HRRGGBB numbers must swap the red and blue bytes, or build its colors with RGB. New: QBCOLOR(n) and the constants vbBlack, vbRed, vbGreen, vbYellow, vbBlue, vbMagenta, vbCyan, and vbWhite.
    +.item Text used as a number must be a number, as in Visual Basic: n = "12" stores 12, but n = "abc", "abc" + 2, or a READ of DATA abc into a number raise error 13 (Type mismatch) and leave the variable as it was. VAL still reads the number at the start of any text, and INPUT reads numbers the way VAL does.
     .item A Long result too large for a Long raises error 6 (Overflow), as in Visual Basic, instead of becoming a Double (an Integer result still becomes a Long). Use CDBL on an operand when a larger result is expected.
     .endlist
     
    @@ -1220,61 +1236,61 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec
     
     .list
     .item GOSUB and RETURN work inside SUB and FUNCTION: each procedure call has its own GOSUB stack. RETURN with no pending GOSUB in the procedure raises error 3 (RETURN without GOSUB); before, RETURN inside a procedure left the procedure (use EXIT SUB / EXIT FUNCTION for that).
    -.item Labels belong to their SUB or FUNCTION (or to the module level). GOTO, GOSUB, ON GOTO/GOSUB and ON ERROR GOTO reach only labels of the same procedure; the same label name can be used in several procedures, and a label may share a name with a variable. An undefined label, or a jump to a SUB or variable name, is a compile error. "SubName:" at the start of a line is an error instead of a silent label.
    +.item Labels belong to their SUB or FUNCTION (or to the module level). GOTO, GOSUB, ON GOTO/GOSUB, and ON ERROR GOTO reach only labels of the same procedure; the same label name can be used in several procedures, and a label may share a name with a variable. An undefined label, or a jump to a SUB or variable name, is a compile error. "SubName:" at the start of a line is an error instead of a silent label.
     .item A CONST inside a SUB or FUNCTION is local and no longer overrides a module constant. CONST name AS type converts the literal (Integer and Long round and range-check); a string/number mismatch or an overflow is a compile error.
     .item DEF FN must be defined before it is used; its free names are module variables.
    -.item DEFINT, DEFLNG and the other DEFtype statements also apply to parameters, STATIC variables and FUNCTION results. A typed FUNCTION converts the value assigned to it (FUNCTION F AS INTEGER with F = 2.5 returns 2). STATIC variables convert on store and their names no longer leak out of the procedure.
    +.item DEFINT, DEFLNG, and the other DEFtype statements also apply to parameters, STATIC variables, and FUNCTION results. A typed FUNCTION converts the value assigned to it (FUNCTION F AS INTEGER with F = 2.5 returns 2). STATIC variables convert on store and their names no longer leak out of the procedure.
     .item DIM a, b AS STRING, c(3) declares several variables in one statement.
     .item name(...) in an expression only calls FUNCTIONs; a SUB there, or indexing a scalar or CONST, is a compile error.
     .item A FUNCTION can be called as a statement (F 3), discarding its result.
     .item DECLARE must match the definition's BYVAL and OPTIONAL flags too.
     .item SWAP and GET #n, , arr(i) evaluate each subscript exactly once. A BYREF argument written as an expression such as arr(i) + 1 is passed by value.
     .item Numeric literals: an Integer/Long suffix on a literal with a fraction or exponent rounds (1E3% is 1000); -32768% and -2147483648& are accepted; 1. is a valid literal; an over-long literal is an error.
    -.item DATA: unquoted items are raw text up to the next comma, colon, comment or end of line.
    -.item READ, INPUT, INPUT # and LINE INPUT # accept array elements and TYPE fields; PRINT #n, alone writes an empty line.
    +.item DATA: unquoted items are raw text up to the next comma, colon, comment, or end of line.
    +.item READ, INPUT, INPUT #, and LINE INPUT # accept array elements and TYPE fields; PRINT #n, alone writes an empty line.
     .item PRINT USING: ; and , both separate values, and a trailing ; or , suppresses the newline.
     .item More than 8 array dimensions, or an LBOUND/UBOUND dimension outside 1..8, is a compile error.
    -.item A missing END SUB, END FUNCTION, END TYPE or END DECLARE is a compile error.
    +.item A missing END SUB, END FUNCTION, END TYPE, or END DECLARE is a compile error.
     .item FOR: a constant fractional STEP with an Integer or Long counter is a compile error, and a computed STEP is rounded to the counter's type. A fractional STEP no longer turns a Single counter into a Double.
     .item DECLARE LIBRARY arguments are always passed by value, converted to the declared parameter type (a Single parameter receives a Single).
    -.item Name(idx).Member works with any member name (including keywords such as Name and Text), with method calls and in chains; Me.Method (x) is a method call.
    +.item Name(idx).Member works with any member name (including keywords such as Name and Text), with method calls, and in chains; Me.Method (x) is a method call.
     .item Compile errors read "Line N, Col C: message" and point at what is wrong (the column is left out only at the end of the source). A SUB called but never defined is reported where it is called.
    -.item Built-in function names are not reserved: a variable, constant, SUB or FUNCTION the program declares with the same name is used instead (see How Names Are Found in the language reference).
    -.item Assigning a whole TYPE value to a TYPE variable, array element or field copies it into the record that is there, as in Visual Basic, so a SUB that was passed one of its fields by reference sees the new value.
    +.item Built-in function names are not reserved: a variable, constant, SUB, or FUNCTION the program declares with the same name is used instead (see How Names Are Found in the language reference).
    +.item Assigning a whole TYPE value to a TYPE variable, array element, or field copies it into the record that is there, as in Visual Basic, so a SUB that was passed one of its fields by reference sees the new value.
     .item A program's compiled code is no longer limited to 64 KB, and programs compile faster with less memory (a program with many procedures twice as fast).
     .item New: COMMAND$ returns the arguments the program was started with (for example the document it was opened with), and COMMAND$(n) returns the nth argument without its quotes. The DVX programs that open documents (Notepad, Image Viewer, Icon Editor, Resource Editor, Help Editor) open the file named there when they start. In the IDE, set the arguments under Project Properties. A program that uses COMMAND$ does not run on an older DVX.
    -.item A SUB or FUNCTION may use at most 64 local slots (parameters, local variables and hidden temporaries). More is a compile error "Too many local variables in X (n, limit is 64)" instead of a runtime error 9; READ and INPUT # into many array elements no longer use a slot per target.
    +.item A SUB or FUNCTION may use at most 64 local slots (parameters, local variables, and hidden temporaries). More is a compile error "Too many local variables in X (n, limit is 64)" instead of a runtime error 9; READ and INPUT # into many array elements no longer use a slot per target.
     .item SHARED naming a variable that does not exist at module level creates it there; in a form's code it becomes a variable of that form, not a program-wide one.
    -.item New types Byte (0 to 255) and Currency (an exact amount with four decimal places, suffix @), with CBYTE, CCUR, DEFBYTE and DEFCUR. A variable declared with a type has that type before anything is stored in it (VARTYPE of an unassigned Long is vbLong).
    -.item Named arguments, as in Visual Basic: Report title:="Sales", width:=60 for SUBs, FUNCTIONs, class methods and the built-in functions that have argument names (MsgBox, InputBox, Mid, InStr, Format, DateAdd and others).
    -.item QBasic line numbers: a line may start with a number from 0 to 65529, which GOTO, GOSUB, ON GOTO, IF ... THEN 100, RESTORE and RESUME can name; ERL gives the last line number before the error. RESTORE label and RESUME label are new too.
    +.item New types Byte (0 to 255) and Currency (an exact amount with four decimal places, suffix @), with CBYTE, CCUR, DEFBYTE, and DEFCUR. A variable declared with a type has that type before anything is stored in it (VARTYPE of an unassigned Long is vbLong).
    +.item Named arguments, as in Visual Basic: Report title:="Sales", width:=60 for SUBs, FUNCTIONs, class methods, and the built-in functions that have argument names (MsgBox, InputBox, Mid, InStr, Format, DateAdd, and others).
    +.item QBasic line numbers: a line may start with a number from 0 to 65529, which GOTO, GOSUB, ON GOTO, IF ... THEN 100, RESTORE, and RESUME can name; ERL gives the last line number before the error. RESTORE label and RESUME label are new too.
     .item STATIC arrays and STATIC TYPE variables keep their contents between calls.
    -.item Members of an object whose class the compiler knows (declared As the class or As Collection, New, Me in a class) are checked when the program is compiled: a misspelled or Private member, the wrong number of arguments, a Sub used as a value, a property used as a statement or assigned without a Property Let are compile errors that point at the member, and the calls run faster. Through As Object and As Variant members are still found when the program runs. A class's own member variables can be used anywhere in the class, and Me reaches its Private members. Passing a number, text or an object of another class to an object parameter of a known class is a compile error.
    +.item Members of an object whose class the compiler knows (declared As the class or As Collection, New, Me in a class) are checked when the program is compiled: a misspelled or Private member, the wrong number of arguments, a Sub used as a value, a property used as a statement or assigned without a Property Let are compile errors that point at the member, and the calls run faster. Through As Object and As Variant members are still found when the program runs. A class's own member variables can be used anywhere in the class, and Me reaches its Private members. Passing a number, text, or an object of another class to an object parameter of a known class is a compile error.
     .endlist
     
    -.h2 Objects, Classes and Variants
    +.h2 Objects, Classes, and Variants
     
     .list
    -.item New: class modules (.cls files) define your own objects, with Public and Private member variables, Subs and Functions, Property Get / Let / Set, Class_Initialize and Class_Terminate, Me and a default member. Objects are made with NEW (DIM x AS NEW Account, SET x = NEW Account), compared with Is and tested with TypeOf ... Is. An object is freed, and its Class_Terminate run, as soon as nothing refers to it any more.
    +.item New: class modules (.cls files) define your own objects, with Public and Private member variables, Subs and Functions, Property Get / Let / Set, Class_Initialize and Class_Terminate, Me, and a default member. Objects are made with NEW (DIM x AS NEW Account, SET x = NEW Account), compared with Is and tested with TypeOf ... Is. An object is freed, and its Class_Terminate run, as soon as nothing refers to it any more.
     .item New: the Collection object (Add, Remove, Item, Count, with optional text keys) and FOR EACH ... NEXT over Collections and arrays.
    -.item New: the Variant type (DIM v AS VARIANT) with the special values Empty and Null, and the functions VARTYPE, TYPENAME, ISEMPTY, ISNULL, ISNUMERIC, ISOBJECT, ISARRAY and CVAR, with the vb... VarType constants and vbObjectError.
    +.item New: the Variant type (DIM v AS VARIANT) with the special values Empty and Null, and the functions VARTYPE, TYPENAME, ISEMPTY, ISNULL, ISNUMERIC, ISOBJECT, ISARRAY, and CVAR, with the vb... VarType constants and vbObjectError.
     .item New: PUBLIC and PRIVATE at module level (PUBLIC x is DIM SHARED x, PRIVATE x is DIM x).
     .item New error numbers: 94 Invalid use of Null, 424 Object required, 438 Object doesn't support this property or method, 450 Wrong number of arguments or invalid property assignment, 451 Object not a collection, 457 duplicate Collection key.
     .item Changed: CHOOSE with an index outside its list returns Null instead of raising error 5.
     .item Changed: a PARAMARRAY without an AS clause is an array of Variants (TYPENAME "Variant()").
     .item A form or control name can be used directly wherever an object is expected: SET c = Text1, either side of Is, TypeOf, and an argument for an object parameter.
    -.item NEW, PROPERTY, PUBLIC, PRIVATE and TYPEOF are now reserved words: a variable, SUB or FUNCTION with one of these names must be renamed. VARIANT, OBJECT, COLLECTION, EMPTY, NULL and EACH are colored like keywords but are not reserved.
    -.item IDE: File > New Class Module, .cls files in Add File, a Classes group in the Project Explorer and a [Classes] section in the project file. The debugger shows Variants, objects and the member variables of the object whose code is running.
    +.item NEW, PROPERTY, PUBLIC, PRIVATE, and TYPEOF are now reserved words: a variable, SUB, or FUNCTION with one of these names must be renamed. VARIANT, OBJECT, COLLECTION, EMPTY, NULL, and EACH are colored like keywords but are not reserved.
    +.item IDE: File > New Class Module, .cls files in Add File, a Classes group in the Project Explorer, and a [Classes] section in the project file. The debugger shows Variants, objects, and the member variables of the object whose code is running.
     .item New: events -- EVENT and RAISEEVENT in class modules, DIM WithEvents variables in modules and forms with handlers named variable_EventName.
     .item New: IMPLEMENTS -- a class can provide the members of another class (the interface) and its objects can be used through variables of the interface type.
     .item New: ISMISSING for OPTIONAL Variant parameters; arguments can be left out in the middle of a call (Report a, , c); FORMAT$ without a format string; LSET of whole TYPE records.
     .item Changed: objects that only refer to each other in a loop are found and freed while the program runs (their Class_Terminate runs first) instead of staying until the program ends.
     .item Changed: an array stored in a Variant is a copy, not shared; ParamArray arguments that are variables are passed by reference; a left-out Variant argument of a method is Missing instead of Empty.
     .item New error numbers: 448 Named argument not found (a Missing value used), 449 Argument not optional.
    -.item EVENT, RAISEEVENT, WITHEVENTS and IMPLEMENTS are colored like keywords but are not reserved.
    -.item New: default values for OPTIONAL parameters (Optional count As Integer = 10), in procedures, class methods and Property procedures.
    +.item EVENT, RAISEEVENT, WITHEVENTS, and IMPLEMENTS are colored like keywords but are not reserved.
    +.item New: default values for OPTIONAL parameters (Optional count As Integer = 10), in procedures, class methods, and Property procedures.
     .item New: a class module can declare WithEvents member variables and handle the events of the objects it holds; listening never keeps an object alive.
    -.item Changed: ParamArray arguments that are array elements, TYPE fields or a class's member variables are passed by reference too, and so are variables and array elements given to a class's methods through As Object or As Variant variables (for parameters that are not BYVAL).
    +.item Changed: ParamArray arguments that are array elements, TYPE fields, or a class's member variables are passed by reference too, and so are variables and array elements given to a class's methods through As Object or As Variant variables (for parameters that are not BYVAL).
     .item New: PLAY's X command and "=" numbers with VARPTR$, as in QBasic; each screen page keeps its own text cursor; a PictureBox fires Paint after its size changes.
     .item New: OPTIONAL object parameters can have the default Nothing (Optional target As Object = Nothing).
     .item New: TYPE fields given to a class's methods (obj.Move rec.x) are passed by reference; values a method stores in a variable passed by reference, or copies back from its ParamArray, are converted to the variable's type. The methods of forms and controls take their arguments by value.
    @@ -1282,7 +1298,7 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec
     .item New: LOADPICTURE returns a Picture object for DragIcon and the Picture of Image and ImageButton (file names still work); new error 481 Invalid picture.
     .item New: while a control is dragged, the pointer shows the "no drop" sign over objects that have no DragDrop event procedure and outside the program's forms.
     .item IDE: File > Rename File; WithEvents variables and their class's events in the code window's dropdowns; date literals and the console and graphics statement names are colored.
    -.item Changed: a parameter passed by reference is the caller's variable itself, as in Visual Basic. A TYPE field given to a SUB or FUNCTION (Bump rec.count, Bump recs(2).count) is now passed by reference (it was passed by value); a write reaches the variable at once instead of when the call returns, also through a ParamArray, a class's method and RaiseEvent, so it stays when the procedure later stops with an error and every handler of an event writes to the same variable; a STRING * n variable or field keeps its length at every write. The debugger shows (and changes) the caller's variable for such a parameter.
    +.item Changed: a parameter passed by reference is the caller's variable itself, as in Visual Basic. A TYPE field given to a SUB or FUNCTION (Bump rec.count, Bump recs(2).count) is now passed by reference (it was passed by value); a write reaches the variable at once instead of when the call returns, also through a ParamArray, a class's method, and RaiseEvent, so it stays when the procedure later stops with an error and every handler of an event writes to the same variable; a STRING * n variable or field keeps its length at every write. The debugger shows (and changes) the caller's variable for such a parameter.
     .endlist
     
     .link lang.classes See: Objects and Class Modules
    @@ -1290,18 +1306,18 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec
     .link lang.interfaces See: Interfaces (Implements)
     .link lang.variants See: Variants
     
    -.h2 Sound, Joysticks and Frames
    +.h2 Sound, Joysticks, and Frames
     
     .list
    -.item New statements from QBasic: BEEP, SOUND frequency, duration (in clock ticks, 18.2 a second) and PLAY with the whole music macro language (notes, octaves, lengths, dots, pauses, tempo, MN/ML/MS, and MF/MB for foreground or background music), plus the PLAY(n) function (notes still waiting in the background). They play on whatever sound hardware the computer has, or silently when there is none.
    +.item New statements from QBasic: BEEP, SOUND frequency, duration (in clock ticks, 18.2 a second), and PLAY with the whole music macro language (notes, octaves, lengths, dots, pauses, tempo, MN/ML/MS, and MF/MB for foreground or background music), plus the PLAY(n) function (notes still waiting in the background). They play on whatever sound hardware the computer has, or silently when there is none.
     .item PLAY's new I command plays the notes that follow on a General MIDI instrument (I1 to I128) through the music synthesizer; I0 goes back to tones.
     .item New functions from QBasic: STICK(n) (joystick position, 1 to 200) and STRIG(n) (joystick buttons), on any joystick DVX knows, or the keyboard when there is none.
     .item New statement FRAMEWAIT rate: waits for the next frame of a steady clock, keeping the desktop running, so a game loop runs at the same speed on every computer.
    -.item BEEP, SOUND, PLAY and FRAMEWAIT are now keywords, and STICK and STRIG built-in functions: a variable, SUB or FUNCTION with one of these names must be renamed.
    +.item BEEP, SOUND, PLAY, and FRAMEWAIT are now keywords, and STICK and STRIG built-in functions: a variable, SUB, or FUNCTION with one of these names must be renamed.
     .item Message boxes (MsgBox) play the desktop sound chosen for their kind on the Control Panel's Sounds tab.
     .endlist
     
    -.link lang.sound See: Sound, Joysticks and Frames
    +.link lang.sound See: Sound, Joysticks, and Frames
     
     .h2 Runtime
     
    @@ -1311,8 +1327,8 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec
     .item RESUME NEXT after an error in an IF/ELSEIF/WHILE/DO/LOOP condition skips the block or leaves the loop; after an error in a FOR statement the loop is skipped; after an error in the SELECT CASE expression the whole SELECT CASE block is skipped; after an error in a CASE test that test counts as false and the next value or CASE is tried; after an error in an ON selector no branch is taken.
     .item New error numbers: 3 RETURN without GOSUB, 14 Out of string space, 28 Out of stack space (was reported as 7), 63 Bad record number, 91 Object variable not set. ERR is a Long, and ERROR n reports the standard message for known numbers.
     .item Using a property or method (or Show, Hide, Unload) on a value that is not a form or control raises error 91 instead of doing nothing.
    -.item EXP, ^ and other math that leaves the numeric range raise error 6 (Overflow); an undefined result raises error 5.
    -.item INT, FIX and SGN return an Integer (Long or Double when needed); RND returns a Double and follows QBasic (RND(0) repeats the last number, a negative argument reseeds). RANDOMIZE with a negative or very large seed, and RND with a very large negative argument, seed predictably (the seed is taken modulo 2^32), so the sequence for a negative RANDOMIZE seed may differ from before. -x of the most negative Integer or Long widens like 0 - x.
    +.item EXP, ^, and other math that leaves the numeric range raise error 6 (Overflow); an undefined result raises error 5.
    +.item INT, FIX, and SGN return an Integer (Long or Double when needed); RND returns a Double and follows QBasic (RND(0) repeats the last number, a negative argument reseeds). RANDOMIZE with a negative or very large seed, and RND with a very large negative argument, seed predictably (the seed is taken modulo 2^32), so the sequence for a negative RANDOMIZE seed may differ from before. -x of the most negative Integer or Long widens like 0 - x.
     .item INSTR follows OPTION COMPARE TEXT. INSTR and the MID$ statement reject a start below 1 (error 5); MID$ with a length of 0 replaces nothing.
     .item PRINT # writes a space after numbers, as PRINT does. PRINT USING and FORMAT$ show a value too wide for the field in scientific notation.
     .item SEEK on a RANDOM file works in records (statement and function); a position below 1 is error 63. PUT of a value longer than the record length raises error 59 before writing anything.
    @@ -1321,13 +1337,13 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec
     .item IniRead and IniWrite use the same INI reader as the DVX system settings. IniWrite keeps the text of every other line (of any length) unchanged, ends every line of the saved file with CR LF, and raises error 75 when it cannot read the existing file or write the new one (a write failure used to be ignored); a failed write leaves the old file intact. IniRead no longer finds keys in the tail of a long line, and keys above the first section header are read with section "". Section names are matched with surrounding blanks ignored.
     .item VAL keeps the magnitude of very long digit strings. Running out of memory while building a string raises error 14.
     .item REDIM PRESERVE of a TYPE array keeps the existing elements.
    -.item Appending to a String variable with s = s + x or s = s & x no longer copies the whole string each time, so building long strings in a loop is much faster. When x is InputBox$, MsgBox, CreateForm, CreateControl or Shell, s is still read before x runs (left to right).
    -.item And, Or, Xor, Eqv and Imp of two Booleans give a Boolean, and Not of a Boolean gives a Boolean, as in Visual Basic 4 and later: Print (a > 1) Or (b > 1) prints True instead of -1, and TypeName gives "Boolean". With a number on either side they still work on the bits and give a number.
    -.item Big disks and long file names: where the disk takes long file names (DVX under Windows, or a long file name driver loaded before DVX), OPEN, KILL, NAME, MKDIR, DIR$ and the other file statements use them, and the File Manager, the file dialogs and Terminal downloads keep them. A file can be bigger than 2 GB on a FAT32 disk; LOF, FILELEN, LOC and SEEK raise error 6 (Overflow) for a size or position past 2 GB rather than give a wrong number, and EOF stays right below it. FileOpsFreeBytes and FileOpsTotalBytes are right for disks past 2 GB.
    -.item Paths may be written with / or \ wherever a path is taken (OPEN, KILL, NAME, MKDIR, CHDIR, DIR$, FILELEN and the file and folder functions of the libraries), mixed too; paths DVX gives back (CURDIR$, App.Path, App.Data, the file dialogs, the FileOps results) use \, as DOS shows them. New: FileOpsPathNative$ gives a path of your own that form. DIR$("C:\*.*") lists the root of C: (before, it listed C:'s current folder). SHELL commands still go to DOS as written, where / starts a switch.
    +.item Appending to a String variable with s = s + x or s = s & x no longer copies the whole string each time, so building long strings in a loop is much faster. When x is InputBox$, MsgBox, CreateForm, CreateControl, or Shell, s is still read before x runs (left to right).
    +.item And, Or, Xor, Eqv, and Imp of two Booleans give a Boolean, and Not of a Boolean gives a Boolean, as in Visual Basic 4 and later: Print (a > 1) Or (b > 1) prints True instead of -1, and TypeName gives "Boolean". With a number on either side they still work on the bits and give a number.
    +.item Big disks and long file names: where the disk takes long file names (DVX under Windows, or a long file name driver loaded before DVX), OPEN, KILL, NAME, MKDIR, DIR$, and the other file statements use them, and the File Manager, the file dialogs, and Terminal downloads keep them. A file can be bigger than 2 GB on a FAT32 disk; LOF, FILELEN, LOC, and SEEK raise error 6 (Overflow) for a size or position past 2 GB rather than give a wrong number, and EOF stays right below it. FileOpsFreeBytes and FileOpsTotalBytes are right for disks past 2 GB.
    +.item Paths may be written with / or \ wherever a path is taken (OPEN, KILL, NAME, MKDIR, CHDIR, DIR$, FILELEN, and the file and folder functions of the libraries), mixed too; paths DVX gives back (CURDIR$, App.Path, App.Data, the file dialogs, the FileOps results) use \, as DOS shows them. New: FileOpsPathNative$ gives a path of your own that form. DIR$("C:\*.*") lists the root of C: (before, it listed C:'s current folder). SHELL commands still go to DOS as written, where / starts a switch.
     .item The IDE shows a project's file paths with \ and keeps writing them with / in the .dbp file, so a project reads the same everywhere.
    -.item The Open and Save As dialogs (DlgFileOpen$, DlgFileSave$ and the IDE's own) have a Drive list beside the Path box (Alt+D); a drive with no disk in it says it is not ready.
    -.item Changed: FileOpsValidName takes the folder the name is for, FileOpsValidName(folder, itemName), since the rules depend on its disk; XferSafeName$ takes the folder a file is received into, XferSafeName$(name, folder). New: FileOpsLongNames, FileOpsShortPath$, FileOpsValidLongName and FileOpsValidShortName (fileops.bas), and ShellArgQuote$ (shell.bas), which puts a path with spaces in quotes for a command line.
    +.item The Open and Save As dialogs (DlgFileOpen$, DlgFileSave$, and the IDE's own) have a Drive list beside the Path box (Alt+D); a drive with no disk in it says it is not ready.
    +.item Changed: FileOpsValidName takes the folder the name is for, FileOpsValidName(folder, itemName), since the rules depend on its disk; XferSafeName$ takes the folder a file is received into, XferSafeName$(name, folder). New: FileOpsLongNames, FileOpsShortPath$, FileOpsValidLongName, and FileOpsValidShortName (fileops.bas), and ShellArgQuote$ (shell.bas), which puts a path with spaces in quotes for a command line.
     .item Programs compiled with an older release must be rebuilt. A program whose procedure table records more than 64 locals for a procedure is refused when it is loaded.
     .item A class may have at most 65536 member variables and a form at most 65536 form-level variables (as many as the compiled program can number); more is a compile error ("Too many member variables in X", "Too many form-level variables"), and a program file that records more is refused when it is loaded.
     .endlist
    @@ -1338,11 +1354,12 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec
     .item A reference to a removed control or to a control of an unloaded form raises an "unknown control" runtime error. CreateControl with a parent that is not a container on the same form raises "CreateControl: invalid parent".
     .item SetEvent works on forms (Me) and menu items; a handler without parameters is called without the event's arguments.
     .item Frame: the Caption from the .frm file or set by code now shows on the frame's border, and reading Caption gives it back (it was ignored and read back "").
    -.item A control's Index below 0, in a .frm file or typed in the designer, means the control is not in a control array, in the designer, the form loader and the saved file alike (a negative Index was kept, saved without an Index line and read back as a different control).
    +.item A control's Index below 0, in a .frm file or typed in the designer, means the control is not in a control array, in the designer, the form loader, and the saved file alike (a negative Index was kept, saved without an Index line, and read back as a different control).
     .item ListBox: a horizontal scroll bar appears when the box is narrower than its longest item (its MaxWidth, or a narrow form); Left and Right scroll it a character at a time.
    -.item Padding = 0 and Spacing = 0 now mean no padding and no space (before, 0 meant the default). A form, box or ScrollPane that sets none keeps its own (4 pixels for forms, VBox and HBox; a Frame's Spacing is 0, a ScrollPane's both 0), and reading Padding or Spacing gives the pixels in use. In the designer, an empty Padding or Spacing on the form means the layout's own.
    +.item Padding = 0 and Spacing = 0 now mean no padding and no space (before, 0 meant the default). A form, box, or ScrollPane that sets none keeps its own (4 pixels for forms, VBox, and HBox; a Frame's Spacing is 0, a ScrollPane's both 0), and reading Padding or Spacing gives the pixels in use. In the designer, an empty Padding or Spacing on the form means the layout's own.
     .item MaxWidth and MaxHeight win over MinWidth / MinHeight and over the size a control's contents ask for, and the space above the cap goes to the other controls instead of staying empty.
    -.item New drawing members on forms and PictureBoxes: FontScale (Print, TextWidth and TextHeight up to 8 times as large, for titles) and ScrollBarsNeeded (which scroll bars content of a size needs in an area, as the controls decide it; vbSBNone, vbHorizontal, vbVertical, vbBoth).
    +.item New drawing members on forms and PictureBoxes: FontScale (Print, TextWidth, and TextHeight up to 8 times as large, for titles) and ScrollBarsNeeded (which scroll bars content of a size needs in an area, as the controls decide it; vbSBNone, vbHorizontal, vbVertical, vbBoth).
    +.item Fonts: every control and form has FontName, FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru, as in Visual Basic. A control without a font of its own uses its container's or its form's. Print, TextWidth, TextHeight, PrintAccel, and TextWidthAccel on forms and PictureBoxes use the object's font. The new DlgFont in commdlg.bas shows the font dialog.
     .item Form.Visible = True/False behaves exactly like Show/Hide, but only on a real change: Visible = True on a form that is already shown does nothing (a modal form stays modal), and on a form that has no window it is ignored. MinWidth and MinHeight read back the value that was set. Size properties are limited to 0..32767.
     .item Disabling a container (for example Frame.Enabled = False) blocks mouse and keyboard input to every control inside it (also inside a disabled ScrollPane or Splitter), and SetFocus on such a control does nothing.
     .item OptionButton.Value is a read/write Boolean (True = selected); the button's index is the new read-only OptionIndex. Setting Value raises no Click.
    @@ -1350,14 +1367,14 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec
     .item ListBox SetItems "" empties the list, and every | separates items. ListView SetColumns has no length limit. TreeView Clear resets scrolling. TextArea FindNext finds a match that starts at the cursor, and the new SetCursor method moves the caret without selecting.
     .item TabStrip/TabControl: a negative tab index selects the first tab and an index past the end the last one.
     .item The mouse wheel over a DBGrid scrolls the rows without moving the current record, and over a Terminal scrolls its history locally.
    -.item .frm files: End followed by blanks or a comment (also End'comment) closes its block while End followed by a word does not, in the compiler, the build and the release renamer exactly as in the loader, Begin followed by a tab is accepted, Enabled = False on popup-menu items is applied, properties after a nested child's End apply to the parent, and Layout on a control that is not a container is reported.
    -.item Include libraries: sql.bas, comm.bas and commdlg.bas take and return Long (SQLFieldInt and SQLAffected no longer wrap above 32767); resource.bas string functions are declared AS STRING. CommHandshake takes an optional passphrase, and the new CommSetPlain accepts unencrypted data on a channel.
    -.item commdlg.bas functions are renamed to match the other include libraries: basFileOpen is now DlgFileOpen$, basFileSave is DlgFileSave$, basInputBox2 is DlgInputBox$, basInputCancelled is DlgInputCancelled, basChoiceDialog is DlgChoice, basIntInput is DlgIntInput and basPromptSave is DlgPromptSave. The old names are gone; programs that use them must be updated and rebuilt.
    +.item .frm files: End followed by blanks or a comment (also End'comment) closes its block while End followed by a word does not, in the compiler, the build, and the release renamer exactly as in the loader, Begin followed by a tab is accepted, Enabled = False on popup-menu items is applied, properties after a nested child's End apply to the parent, and Layout on a control that is not a container is reported.
    +.item Include libraries: sql.bas, comm.bas, and commdlg.bas take and return Long (SQLFieldInt and SQLAffected no longer wrap above 32767); resource.bas string functions are declared AS STRING. CommHandshake takes an optional passphrase, and the new CommSetPlain accepts unencrypted data on a channel.
    +.item commdlg.bas functions are renamed to match the other include libraries: basFileOpen is now DlgFileOpen$, basFileSave is DlgFileSave$, basInputBox2 is DlgInputBox$, basInputCancelled is DlgInputCancelled, basChoiceDialog is DlgChoice, basIntInput is DlgIntInput, and basPromptSave is DlgPromptSave. The old names are gone; programs that use them must be updated and rebuilt.
     .item New in comm.bas: CommRecvEncrypted(handle, channel) returns True when every byte the last CommRecv$ on that channel returned arrived encrypted and authenticated (also when it returned nothing), and False when any arrived unencrypted (before CommHandshake or on a CommSetPlain channel).
    -.item KeyDown: Shift, Ctrl or Alt with F1-F12, and Alt with Home, End, PgUp, PgDn, Ins, Del and the arrow keys, report the plain key's KeyCode with the modifier in Shift, even when the modifier is released before the program sees the key.
    -.item Data control: while its Validate handler runs, MoveFirst, MoveLast, MoveNext, MovePrevious, AddNew, Refresh and Update on the same Data control are ignored (they used to validate the same record again without end); Delete is allowed. Only unloading the Data control itself from Validate or Reposition stops the operation that fired it; removing other controls does not.
    +.item KeyDown: Shift, Ctrl, or Alt with F1-F12, and Alt with Home, End, PgUp, PgDn, Ins, Del, and the arrow keys, report the plain key's KeyCode with the modifier in Shift, even when the modifier is released before the program sees the key.
    +.item Data control: while its Validate handler runs, MoveFirst, MoveLast, MoveNext, MovePrevious, AddNew, Refresh, and Update on the same Data control are ignored (they used to validate the same record again without end); Delete is allowed. Only unloading the Data control itself from Validate or Reposition stops the operation that fired it; removing other controls does not.
     .item Data control: writing a field back with the value it already has is not an edit, so leaving a bound control no longer fires Validate. With a SELECT query as RecordSource, edits are dropped when the current record changes. Update and Delete still find a record whose key is a number stored without a declared column type, or is NULL, when the key names exactly one record; Delete keeps the record shown when the database row could not be deleted.
    -.item DBGrid re-sorts and re-measures its columns when the Data control's records change (Refresh, AddNew, Delete or an edit), keeping the chosen sort column, and not on a plain record move.
    +.item DBGrid re-sorts and re-measures its columns when the Data control's records change (Refresh, AddNew, Delete, or an edit), keeping the chosen sort column, and not on a plain record move.
     .item Image and ImageButton: Picture = "" clears the picture (Picture then reads back ""); a file that cannot be loaded leaves the picture and Picture unchanged. A disabled Image keeps its transparent color transparent.
     .item Timer: a Timer whose handler shows a modal dialog no longer fires a second time from inside that dialog.
     .item TextArea: the horizontal scroll bar appears as soon as the longest line fills the visible width, and the caret stays visible while typing at the right edge.
    @@ -1366,7 +1383,7 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec
     .item TabControl: switching tabs closes an open drop-down list only when it belongs to a control on the page being hidden. A Label on a Toolbar is drawn on the toolbar's color.
     .item Compile errors from the command-line compiler name the file and line. A .frm without a Begin Form line is an error. A release build that runs out of memory while renaming names fails instead of producing a broken program.
     .item A form shown modal has no minimize gadget.
    -.item Menu items accept only Name (read-only), Checked and Enabled in code; Caption, Visible, RadioCheck, Shortcut and ShortcutText are design-time settings, and using them in code is a compile error in the IDE.
    +.item Menu items accept only Name (read-only), Checked, and Enabled in code; Caption, Visible, RadioCheck, Shortcut, and ShortcutText are design-time settings, and using them in code is a compile error in the IDE.
     .endlist
     
     .h2 IDE
    @@ -1392,6 +1409,8 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec
     .item The title bar's * also shows for a form changed in the designer but not yet saved.
     .item Form designer: a .frm whose container has properties after the controls inside it (Index, Caption ...; DVX writes them before) opens as it runs: the properties are kept and the controls stay in that container. Before, they were dropped, and such a file with another control of the container's name opened as a blank form. Menu items' properties after their submenus are kept too. FrmOpen reads .frm files the same way.
     .item The Toolbox lists the controls in alphabetical order, as CtlTypeName$ does.
    +.item Properties window: the Font row sets a control's or the form's font in the Font dialog, and the design surface shows the fonts.
    +.item Toolbox: it can be resized, its buttons rearrange to fit, and a scroll bar appears when they do not all fit (with many controls installed, the bottom rows used to be off the screen).
     .item UserControls: File > New UserControl, a Controls group in the Project Explorer, the UserControl object and its events in the Code Editor, the header in the Properties window, the project's UserControls in the Toolbox and on the design surface (their code runs, with UserMode False), and control projects with File > Make Control. A program built with Make Executable carries its UserControls. See UserControls in the IDE.
     .item Form designer: a property value may be as long as a full .frm line (511 characters); a value too long for the line it is saved on (deeply nested controls) stops the save with "A property value is too long to save at its position in the form". Making a container part of a control array keeps its contents. Only one OptionButton per container keeps Value = True: setting it on one (in the property grid or by pasting a selected one) clears the others.
     .endlist
    diff --git a/src/apps/kpunch/dvxbasic/langref.dhs b/src/apps/kpunch/dvxbasic/langref.dhs
    index 4dd74109..8a5f796e 100644
    --- a/src/apps/kpunch/dvxbasic/langref.dhs
    +++ b/src/apps/kpunch/dvxbasic/langref.dhs
    @@ -71,7 +71,7 @@ An object variable does not hold a value of its own; it refers to an object that
       -----------     -----
       Control         A control on a form (any type of control)
       Form            A form
    -  Object          Any object: a form, a control, an instance of a class module or a Collection
    +  Object          Any object: a form, a control, an instance of a class module, or a Collection
       ClassName       An instance of that class module (see Objects and Class Modules)
       Collection      A Collection object (see Collection Object)
     .endtable
    @@ -85,7 +85,7 @@ Dim people As New Collection
     people.Add "Ann"
     .endcode
     
    -An object variable that has not been Set holds Nothing. Control, Form and Object variables accept each other's values; a variable declared As a class name or As Collection accepts only an object of that kind, or Nothing: an object of another kind raises error 13 (Type mismatch), and a value that is not an object at all raises error 424 (Object required). The Source parameter of the DragDrop and DragOver events is a Control.
    +An object variable that has not been Set holds Nothing. Control, Form, and Object variables accept each other's values; a variable declared As a class name or As Collection accepts only an object of that kind, or Nothing: an object of another kind raises error 13 (Type mismatch), and a value that is not an object at all raises error 424 (Object required). The Source parameter of the DragDrop and DragOver events is a Control.
     
     .link lang.classes See also: Objects and Class Modules
     
    @@ -98,8 +98,8 @@ These types are not directly declarable but are used internally by the runtime.
       -------------   -----------
       Array           Reference-counted multi-dimensional array (up to 8 dimensions)
       UDT             User-defined type instance (created with TYPE...END TYPE)
    -  Object          Reference to a form, a control, a class instance or a Collection; declarable as Control, Form, Object, a class name or Collection
    -  Ref             Reference to a variable, array element, TYPE field or class member variable (what a ByRef parameter holds)
    +  Object          Reference to a form, a control, a class instance, or a Collection; declarable as Control, Form, Object, a class name, or Collection
    +  Ref             Reference to a variable, array element, TYPE field, or class member variable (what a ByRef parameter holds)
     .endtable
     
     .h2 Type Suffixes
    @@ -140,13 +140,13 @@ A literal with an Integer or Long suffix and a fraction or exponent is rounded (
     
     .h2 Type Promotion
     
    -When mixing types in expressions, values are automatically promoted to a common type: Byte -> Integer -> Long -> Single -> Double. A Currency with a Byte, Integer, Long or Currency stays Currency (see Byte and Currency below). Whole-number arithmetic keeps the type of its operands: an Integer result too large for an Integer becomes a Long (32767 + 1 is the Long 32768) and a Byte result outside 0 to 255 becomes an Integer, but a Long result too large for a Long raises error 6 (Overflow), as in Visual Basic. Convert an operand when a larger result is expected: CDBL(2147483647) + 1 is 2147483648.
    +When mixing types in expressions, values are automatically promoted to a common type: Byte -> Integer -> Long -> Single -> Double. A Currency with a Byte, Integer, Long, or Currency stays Currency (see Byte and Currency below). Whole-number arithmetic keeps the type of its operands: an Integer result too large for an Integer becomes a Long (32767 + 1 is the Long 32768) and a Byte result outside 0 to 255 becomes an Integer, but a Long result too large for a Long raises error 6 (Overflow), as in Visual Basic. Convert an operand when a larger result is expected: CDBL(2147483647) + 1 is 2147483648.
     
     + joins two strings. When a string meets a number in an arithmetic operator (including +) or a comparison, the string must hold a number, as in Visual Basic: "1" + 2 is 3 and " 2.5 " * 2 is 5, but text that is not a number ("abc" + 2, "12abc" + 2) raises error 13 (Type mismatch). VAL reads the number at the start of any text instead (VAL("12abc") is 12). Use & to join text and numbers, and VAL / STR$ to convert explicitly.
     
     .h2 Assignment Conversion
     
    -Storing a value into a variable declared as Byte, Integer, Long, Single, Double, Currency, String, Boolean or Date (by suffix, by AS clause, by DEFtype, or as a typed parameter, STATIC variable, FUNCTION result, TYPE field or array element) converts the value to that type: a number stored in a String variable becomes its text as STR$ gives it without the leading space ("5"). Text stored in a numeric variable must be a number, as in Visual Basic: "12" and " 3.5 " convert, while text that is not a number ("abc", "12abc") raises error 13 (Type mismatch) and the variable keeps its value. DEFtype statements also give parameters, STATIC variables and FUNCTION results their default type. Byte, Integer and Long round to the nearest whole number, with halves rounding to the nearest even number (2.5 becomes 2, 3.5 becomes 4), and raise error 6 (Overflow) when the result does not fit; Currency rounds to four decimal places the same way. Single keeps about 7 significant digits, so PRINT shows at most 7 digits of it. Boolean stores True for any non-zero number or non-empty string and False otherwise. Date takes a number as a date's number of days and reads a string as a date (error 13 when the text is not a date, error 6 outside 1/1/100 to 12/31/9999); INPUT into a Date variable reads the date the same way. A variable declared with DIM but without a type is Single (or the DEFtype for its first letter). A variable that is never declared keeps whatever type the assigned value has. A variable declared with a type holds that type from the start, before anything is stored in it: VARTYPE of a Long that was never assigned is 3 (vbLong) and of a String 8 (vbString).
    +Storing a value into a variable declared as Byte, Integer, Long, Single, Double, Currency, String, Boolean, or Date (by suffix, by AS clause, by DEFtype, or as a typed parameter, STATIC variable, FUNCTION result, TYPE field, or array element) converts the value to that type: a number stored in a String variable becomes its text as STR$ gives it without the leading space ("5"). Text stored in a numeric variable must be a number, as in Visual Basic: "12" and " 3.5 " convert, while text that is not a number ("abc", "12abc") raises error 13 (Type mismatch) and the variable keeps its value. DEFtype statements also give parameters, STATIC variables, and FUNCTION results their default type. Byte, Integer, and Long round to the nearest whole number, with halves rounding to the nearest even number (2.5 becomes 2, 3.5 becomes 4), and raise error 6 (Overflow) when the result does not fit; Currency rounds to four decimal places the same way. Single keeps about 7 significant digits, so PRINT shows at most 7 digits of it. Boolean stores True for any non-zero number or non-empty string and False otherwise. Date takes a number as a date's number of days and reads a string as a date (error 13 when the text is not a date, error 6 outside 1/1/100 to 12/31/9999); INPUT into a Date variable reads the date the same way. A variable declared with DIM but without a type is Single (or the DEFtype for its first letter). A variable that is never declared keeps whatever type the assigned value has. A variable declared with a type holds that type from the start, before anything is stored in it: VARTYPE of a Long that was never assigned is 3 (vbLong) and of a String 8 (vbString).
     
     .code
     Dim n As Integer
    @@ -169,7 +169,7 @@ Boolean values use -1 for True and 0 for False. Any non-zero numeric value is tr
     
     A Byte holds a whole number from 0 to 255 in one byte. Storing a value outside that range raises error 6 (Overflow). Arithmetic on two Bytes gives a Byte when the result is 0 to 255 and an Integer otherwise; negating a Byte gives an Integer. CBYTE converts a value to a Byte.
     
    -A Currency holds an amount of money exactly: it counts ten-thousandths, so it has four decimal places and adding, subtracting or multiplying amounts never drifts (adding 0.1@ ten times gives exactly 1). A value with more decimal places is rounded to four, halves to the nearest even digit (CCUR(2.00005) is 2, CCUR(2.00015) is 2.0002), and a result outside the Currency range raises error 6 (Overflow). +, - and * on a Currency and a Byte, Integer, Long or Currency give a Currency; with a Single or Double they give a Double, and / always gives a Double. Comparisons of Currency values are exact. PRINT and STR$ show a Currency with only the decimals it needs (12.5); FORMAT$ lays it out like any other number. A FOR loop with a Currency counter steps exactly. CCUR converts a value to a Currency.
    +A Currency holds an amount of money exactly: it counts ten-thousandths, so it has four decimal places and adding, subtracting, or multiplying amounts never drifts (adding 0.1@ ten times gives exactly 1). A value with more decimal places is rounded to four, halves to the nearest even digit (CCUR(2.00005) is 2, CCUR(2.00015) is 2.0002), and a result outside the Currency range raises error 6 (Overflow). +, -, and * on a Currency and a Byte, Integer, Long, or Currency give a Currency; with a Single or Double they give a Double, and / always gives a Double. Comparisons of Currency values are exact. PRINT and STR$ show a Currency with only the decimals it needs (12.5); FORMAT$ lays it out like any other number. A FOR loop with a Currency counter steps exactly. CCUR converts a value to a Currency.
     
     .code
     Dim total As Currency
    @@ -199,7 +199,7 @@ d = #3:30 PM#                    ' a time alone is on 12/30/1899
     
     Two-digit years 00 to 29 are 2000 to 2029 and 30 to 99 are 1930 to 1999. Date is not a reserved word, so a program may still use Date (or any of the date function names) for its own variables and procedures; the program's own name wins.
     
    -PRINT, STR$, CSTR and the & operator show a date as m/d/yyyy h:mm:ss AM (1/15/2000 3:30:00 PM). A date at midnight shows only the date and a time on 12/30/1899 only the time; a Date variable that was never assigned is 12:00:00 AM. Use FORMAT$ for any other layout.
    +PRINT, STR$, CSTR, and the & operator show a date as m/d/yyyy h:mm:ss AM (1/15/2000 3:30:00 PM). A date at midnight shows only the date and a time on 12/30/1899 only the time; a Date variable that was never assigned is 12:00:00 AM. Use FORMAT$ for any other layout.
     
     Adding a number of days to a date, or subtracting one, gives a date (fractions are parts of a day, so d + 0.25 is six hours later); subtracting one date from another gives the number of days between them as a Double. Comparisons put dates in time order, and a string compared with a date is read as a date:
     
    @@ -237,7 +237,7 @@ A Date variable has no Empty or Null state (a Variant does): a date that is out
     
     .h1 Variants
     
    -A Variant is a variable that can hold any kind of value: a number, text, a date, True or False, an object or a whole array. It takes on the type of whatever is stored in it, and can change type each time something new is stored. A Variant can also hold two special values that no other type has: Empty (nothing has been stored yet) and Null (the value is unknown or missing).
    +A Variant is a variable that can hold any kind of value: a number, text, a date, True or False, an object, or a whole array. It takes on the type of whatever is stored in it, and can change type each time something new is stored. A Variant can also hold two special values that no other type has: Empty (nothing has been stored yet) and Null (the value is unknown or missing).
     
     .code
     Dim v As Variant
    @@ -254,7 +254,7 @@ Print IsNull(v)         ' True
     
     Declare a Variant with AS VARIANT. Arrays of Variants (DIM list(10) AS VARIANT), Variant parameters, and FUNCTIONs and properties AS VARIANT work the same way; each element of a Variant array starts Empty. A variable declared with DIM but no AS clause, or never declared at all, is not a Variant: it keeps the older rules described under Assignment Conversion.
     
    -Storing a Variant in a variable of another type converts the value to that type, as for any other assignment. A Variant that is given an array gets its own copy of the array, as in Visual Basic: after v = list, changing list(1) does not change v(1), and w = v copies again. The same holds when a Variant holding an array is passed BYVAL, stored in an object's member or added to a Collection. Index it like the array itself, v(1).
    +Storing a Variant in a variable of another type converts the value to that type, as for any other assignment. A Variant that is given an array gets its own copy of the array, as in Visual Basic: after v = list, changing list(1) does not change v(1), and w = v copies again. The same holds when a Variant holding an array is passed BYVAL, stored in an object's member, or added to a Collection. Index it like the array itself, v(1).
     
     .code
     Dim list(2) As Integer, v As Variant
    @@ -279,23 +279,23 @@ If IsEmpty(v) Then Print "Nothing stored yet"
     Null means "no valid value" -- for example a field that was never filled in. Null spreads through most expressions: anything calculated from a Null is Null too, so a missing value is not mistaken for a real one.
     
     .table
    -  Expression                                    Result
    -  ----------                                    ------
    -  Null + 1, Null * x, -Null, Not Null           Null
    -  Null = 1, Null < x, Null = Null               Null (a comparison with Null is never True)
    -  Abs(Null), Int(Null), Fix(Null), Sgn(Null)    Null
    -  Left(Null, 1), UCase(Null), Len(Null), ...    Null (the Variant string functions; see below)
    -  "Name: " & Null                               "Name: " (& treats Null as "")
    -  Null & Null                                   Null
    -  False And Null                                False
    -  True Or Null                                  True
    -  False Imp Null, Null Imp True                 True
    -  Any other And, Or, Xor, Eqv or Imp with Null  Null
    +  Expression                                     Result
    +  ----------                                     ------
    +  Null + 1, Null * x, -Null, Not Null            Null
    +  Null = 1, Null < x, Null = Null                Null (a comparison with Null is never True)
    +  Abs(Null), Int(Null), Fix(Null), Sgn(Null)     Null
    +  Left(Null, 1), UCase(Null), Len(Null), ...     Null (the Variant string functions; see below)
    +  "Name: " & Null                                "Name: " (& treats Null as "")
    +  Null & Null                                    Null
    +  False And Null                                 False
    +  True Or Null                                   True
    +  False Imp Null, Null Imp True                  True
    +  Any other And, Or, Xor, Eqv, or Imp with Null  Null
     .endtable
     
    -An IF, ELSEIF, WHILE, DO or LOOP condition that is Null counts as False. Any other use that needs a real number -- storing Null in a numeric, Boolean or Date variable, CINT(Null) and the other conversion functions, a string function ending in $ such as UCASE$ or LEFT$, or an array subscript -- raises error 94 (Invalid use of Null).
    +An IF, ELSEIF, WHILE, DO, or LOOP condition that is Null counts as False. Any other use that needs a real number -- storing Null in a numeric, Boolean, or Date variable, CINT(Null) and the other conversion functions, a string function ending in $ such as UCASE$ or LEFT$, or an array subscript -- raises error 94 (Invalid use of Null).
     
    -As in Visual Basic, the string functions without $ are Variant functions that pass Null through: LEFT, RIGHT, MID, UCASE, LCASE, TRIM, LTRIM, RTRIM, CHR, HEX, OCT, STR, SPACE and STRING give Null when their text (or number) argument is Null, and so do LEN, INSTR, INSTRREV, STRCOMP, LIKE and the date parts YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, WEEKDAY and DATEPART. Their $ forms (LEFT$, UCASE$ ...) always give text and raise error 94 for Null.
    +As in Visual Basic, the string functions without $ are Variant functions that pass Null through: LEFT, RIGHT, MID, UCASE, LCASE, TRIM, LTRIM, RTRIM, CHR, HEX, OCT, STR, SPACE, and STRING give Null when their text (or number) argument is Null, and so do LEN, INSTR, INSTRREV, STRCOMP, LIKE, and the date parts YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, WEEKDAY, and DATEPART. Their $ forms (LEFT$, UCASE$ ...) always give text and raise error 94 for Null.
     
     Because Null = Null is itself Null, test for Null with ISNULL, never with =:
     
    @@ -314,11 +314,11 @@ Empty and Null are not reserved words: a program that declares its own variable
     .table
       Function      Returns   Description
       --------      -------   -----------
    -  VARTYPE(v)    Integer   A number for the kind of value v holds (see VarType Constants): vbEmpty (0), vbNull (1), vbInteger (2), vbString (8), vbObject (9) and so on. For an array it is vbArray (8192) plus the element type, so an Integer array is 8194 and a Variant array 8204.
    +  VARTYPE(v)    Integer   A number for the kind of value v holds (see VarType Constants): vbEmpty (0), vbNull (1), vbInteger (2), vbString (8), vbObject (9), and so on. For an array it is vbArray (8192) plus the element type, so an Integer array is 8194 and a Variant array 8204.
       TYPENAME(v)   String    The name of the kind of value: "Byte", "Integer", "Long", "Single", "Double", "Currency", "String", "Boolean", "Date", "Empty", "Null", "Error" (a left-out OPTIONAL argument), the name of a TYPE, "Integer()" for an array, and for objects the class name, "Collection", "Nothing", or the type of a form or control.
       ISEMPTY(v)    Boolean   True when v is Empty.
       ISNULL(v)     Boolean   True when v is Null.
    -  ISNUMERIC(v)  Boolean   True when v is a number, True or False, Empty, or text that reads as a number ("12", "1.5"). False for Null, dates and other text.
    +  ISNUMERIC(v)  Boolean   True when v is a number, True or False, Empty, or text that reads as a number ("12", "1.5"). False for Null, dates, and other text.
       ISOBJECT(v)   Boolean   True when v is an object reference, including Nothing.
       ISARRAY(v)    Boolean   True when v is an array.
       ISMISSING(v)  Boolean   True when v is an OPTIONAL Variant parameter the caller left out (see Procedures).
    @@ -428,7 +428,7 @@ String comparisons are controlled by OPTION COMPARE. With OPTION COMPARE BINARY
     
     Is compares object references, not values: a Is b is True when both refer to the very same object, and x Is Nothing tests for an object variable that holds no object. Either side may be a form or control name. Using Is on something that is not an object raises error 424 (Object required).
     
    -text Like pattern is True when the whole of text matches the pattern. In the pattern, ? stands for any one character, * for any run of characters (none too), # for one digit 0 to 9, [list] for one character in the list and [!list] for one character not in it; anything else stands for itself. A list holds characters and ranges such as a-z ([A-Za-z0-9_]); to match a literal ?, *, # or [, put it in brackets ([*]). OPTION COMPARE TEXT makes the match ignore case. Like with Null gives Null, and a pattern with a [ that has no ] raises error 93.
    +text Like pattern is True when the whole of text matches the pattern. In the pattern, ? stands for any one character, * for any run of characters (none too), # for one digit 0 to 9, [list] for one character in the list, and [!list] for one character not in it; anything else stands for itself. A list holds characters and ranges such as a-z ([A-Za-z0-9_]); to match a literal ?, *, #, or [, put it in brackets ([*]). OPTION COMPARE TEXT makes the match ignore case. Like with Null gives Null, and a pattern with a [ that has no ] raises error 93.
     
     .code
     If name$ Like "A*" Then Print "Starts with A"
    @@ -500,7 +500,7 @@ REM This is a comment
     .link lang.flow Flow Control (EXIT, CALL, GOTO, GOSUB, ON)
     .link lang.io Input/Output (PRINT, INPUT, DATA/READ)
     .link lang.misc Miscellaneous Statements (ON ERROR, SHELL, SLEEP, END)
    -.link lang.sound Sound, Joysticks and Frames (BEEP, SOUND, PLAY, STICK, STRIG, FRAMEWAIT)
    +.link lang.sound Sound, Joysticks, and Frames (BEEP, SOUND, PLAY, STICK, STRIG, FRAMEWAIT)
     .link lang.classes Objects and Class Modules (NEW, PROPERTY, Class_Initialize, Me)
     .link lang.collection Collection Object (Add, Remove, Item, Count)
     
    @@ -582,7 +582,7 @@ Dim fixedStr As String * 20
     .endcode
     
     .note info
    -A module-level variable is visible inside a SUB or FUNCTION only when it is declared with DIM SHARED (or named in a SHARED statement in that procedure). See Variable Scope below. Inside a SUB or FUNCTION, DIM declares a local variable that is recreated on each call (use STATIC to retain its value between calls). A SUB or FUNCTION holds at most 64 parameters and local variables together, including a few hidden ones that statements such as ON ... GOTO, SWAP and READ into array elements use while they run; a procedure that needs more fails to compile with "Too many local variables". A program holds at most 512 module-level variables, a form at most 65536 form-level variables and a class at most 65536 member variables; more fails to compile.
    +A module-level variable is visible inside a SUB or FUNCTION only when it is declared with DIM SHARED (or named in a SHARED statement in that procedure). See Variable Scope below. Inside a SUB or FUNCTION, DIM declares a local variable that is recreated on each call (use STATIC to retain its value between calls). A SUB or FUNCTION holds at most 64 parameters and local variables together, including a few hidden ones that statements such as ON ... GOTO, SWAP, and READ into array elements use while they run; a procedure that needs more fails to compile with "Too many local variables". A program holds at most 512 module-level variables, a form at most 65536 form-level variables, and a class at most 65536 member variables; more fails to compile.
     .endnote
     
     Fixed-length strings (STRING * n) are padded with spaces and truncated when assigned so their length is always exactly n.
    @@ -598,7 +598,7 @@ DVX BASIC follows the QBasic scope rules:
     .item A SUB or FUNCTION sees a module variable only when it was declared with DIM SHARED at module level, or when the procedure names it in a SHARED statement. This applies to arrays as well.
     .item Any other name used inside a SUB or FUNCTION is a local variable of that procedure, even when a module variable of the same name exists. With OPTION EXPLICIT, using such a name without a DIM is a compile error that says the module variable is not SHARED.
     .item Indexing (name(...)) or dotting (name.member) a module variable the procedure cannot see is a compile error.
    -.item CONSTs declared at module level, TYPEs, SUBs, FUNCTIONs, class modules, forms and controls are visible everywhere; no SHARED is needed for them. PUBLIC at module level is the same as DIM SHARED (see PUBLIC / PRIVATE).
    +.item CONSTs declared at module level, TYPEs, SUBs, FUNCTIONs, class modules, forms, and controls are visible everywhere; no SHARED is needed for them. PUBLIC at module level is the same as DIM SHARED (see PUBLIC / PRIVATE).
     .item A DEF FN is module-level code: its expression sees every module variable.
     .endlist
     
    @@ -633,13 +633,13 @@ SHARED gives the procedure access to the named module variables (write name() fo
     Every name in a program is looked up the same way, in this order:
     
     .list
    -.item A name the program declares is that declaration: first the procedure's own parameters, local variables, STATIC and SHARED names; then, in a class module, the class's own members; then the variables of the form whose code it is; then the module's variables, SUBs, FUNCTIONs, constants, TYPEs, ENUMs and classes, and the predefined vb constants. Inside a procedure a module variable counts only when it is shared (see above); a class's members always do.
    -.item Otherwise a built-in function of that name is used. Built-in names are not reserved: a variable, constant, SUB or FUNCTION the program declares with the same name wins (a FUNCTION named StrReverse replaces the built-in one).
    +.item A name the program declares is that declaration: first the procedure's own parameters, local variables, and STATIC and SHARED names; then, in a class module, the class's own members; then the variables of the form whose code it is; then the module's variables, SUBs, FUNCTIONs, constants, TYPEs, ENUMs, and classes, and the predefined vb constants. Inside a procedure a module variable counts only when it is shared (see above); a class's members always do.
    +.item Otherwise a built-in function of that name is used. Built-in names are not reserved: a variable, constant, SUB, or FUNCTION the program declares with the same name wins (a FUNCTION named StrReverse replaces the built-in one).
     .item Otherwise, where an object is expected -- before a dot (name.Caption), after SET, around Is or TypeOf, or as an argument for an Object or class parameter -- the name is a control of the current form, or a form of that name. Form1.Text1.Text names the control Text1 on the form Form1.
     .item Otherwise the name is a new variable: local inside a procedure, a module variable elsewhere (a DEF FN expression's names are module variables). With OPTION EXPLICIT this is a compile error.
     .endlist
     
    -A SUB or FUNCTION can be called before its definition, and a TYPE, ENUM or class -- with its member variables and procedures -- can be used before it is declared: the compiler reads every declaration first. A name after a dot is a member of the object before it: when that object's class is known (see Objects and Class Modules) the member is looked up in the class when the program is compiled, else when it runs.
    +A SUB or FUNCTION can be called before its definition, and a TYPE, ENUM, or class -- with its member variables and procedures -- can be used before it is declared: the compiler reads every declaration first. A name after a dot is a member of the object before it: when that object's class is known (see Objects and Class Modules) the member is looked up in the class when the program is compiled, else when it runs.
     
     .h2 PUBLIC / PRIVATE
     
    @@ -699,16 +699,16 @@ The value can be any expression built from:
     .item literals (numbers, strings, True and False, #date# literals);
     .item constants declared earlier, including ENUM members (Member or EnumName.Member);
     .item the arithmetic operators + - * / \ MOD ^, the comparison operators, AND OR NOT XOR EQV IMP, and string joining with & or +;
    -.item these built-in functions: ABS, ASC, ATN, CBOOL, CDATE, CDBL, CHOOSE, CHR$, CINT, CLNG, COS, CSNG, CSTR, DATEADD, DATEDIFF, DATEPART, DATESERIAL, DATEVALUE, DAY, EXP, FIX, FORMAT$, GETBLUE, GETGREEN, GETRED, HEX$, HOUR, IIF, INSTR, INSTRREV, INT, ISDATE, LCASE$, LEFT$, LEN, LOG, LTRIM$, MID$, MINUTE, MONTH, MONTHNAME, OCT$, REPLACE$, RGB, RIGHT$, RTRIM$, SECOND, SGN, SIN, SPACE$, SQR, STR$, STRCOMP, STRING$, STRREVERSE$, TAN, TIMESERIAL, TIMEVALUE, TRIM$, UCASE$, VAL, WEEKDAY, WEEKDAYNAME and YEAR.
    +.item these built-in functions: ABS, ASC, ATN, CBOOL, CDATE, CDBL, CHOOSE, CHR$, CINT, CLNG, COS, CSNG, CSTR, DATEADD, DATEDIFF, DATEPART, DATESERIAL, DATEVALUE, DAY, EXP, FIX, FORMAT$, GETBLUE, GETGREEN, GETRED, HEX$, HOUR, IIF, INSTR, INSTRREV, INT, ISDATE, LCASE$, LEFT$, LEN, LOG, LTRIM$, MID$, MINUTE, MONTH, MONTHNAME, OCT$, REPLACE$, RGB, RIGHT$, RTRIM$, SECOND, SGN, SIN, SPACE$, SQR, STR$, STRCOMP, STRING$, STRREVERSE$, TAN, TIMESERIAL, TIMEVALUE, TRIM$, UCASE$, VAL, WEEKDAY, WEEKDAYNAME, and YEAR.
     .endlist
     
    -Anything else -- a variable, a FUNCTION of your own, or a built-in whose answer can change from run to run such as TIMER, NOW, RND or DIR$ -- is a compile error ("Constant expression required"). An expression that fails, such as a division by zero or a result too big for its type, is also reported when the program is compiled. Text comparisons in a constant follow the OPTION COMPARE setting in effect where the constant is declared. A string constant can be at most 255 characters long.
    +Anything else -- a variable, a FUNCTION of your own, or a built-in whose answer can change from run to run such as TIMER, NOW, RND, or DIR$ -- is a compile error ("Constant expression required"). An expression that fails, such as a division by zero or a result too big for its type, is also reported when the program is compiled. Text comparisons in a constant follow the OPTION COMPARE setting in effect where the constant is declared. A string constant can be at most 255 characters long.
     
    -Without an AS clause the constant takes the type of its value: a whole number makes a Long constant, a number with a fraction a Double constant, and strings, Booleans and dates keep their type. With an AS clause the value is converted to that type: Integer and Long values are rounded and must fit the type, and a string for a numeric type (or a number for String) is a compile error.
    +Without an AS clause the constant takes the type of its value: a whole number makes a Long constant, a number with a fraction a Double constant, and strings, Booleans, and dates keep their type. With an AS clause the value is converted to that type: Integer and Long values are rounded and must fit the type, and a string for a numeric type (or a number for String) is a compile error.
     
     Constants declared at module level are visible to all procedures. A CONST declared inside a SUB or FUNCTION is local to that procedure; it may reuse the name of a module constant without changing it.
     
    -PUBLIC CONST and PRIVATE CONST work as in Visual Basic. A Private constant, and any constant of a form or class module, belongs to the file it is declared in: another module, form or class of the project can declare its own constant of the same name, and does not see this one. A Public constant, or a plain CONST in a module, is seen by the whole program. A class module or form cannot have a Public constant ("Constants are not allowed as Public members of a class module or form").
    +PUBLIC CONST and PRIVATE CONST work as in Visual Basic. A Private constant, and any constant of a form or class module, belongs to the file it is declared in: another module, form, or class of the project can declare its own constant of the same name, and does not see this one. A Public constant, or a plain CONST in a module, is seen by the whole program. A class module or form cannot have a Public constant ("Constants are not allowed as Public members of a class module or form").
     
     .code
     Public Const APP_VERSION = "2.1"   ' every file of the project
    @@ -739,7 +739,7 @@ p.firstName = "Scott"
     p.age = 30
     .endcode
     
    -UDT fields can themselves be UDTs (nested types), and field access chains through array elements too: shapes(i).origin.x = 5. A new instance starts with numeric fields at 0, Boolean fields False, variable strings empty and STRING * n fields padded to n spaces. TYPE variables can be written to and read from RANDOM and BINARY files with PUT and GET (see GET / PUT).
    +UDT fields can themselves be UDTs (nested types), and field access chains through array elements too: shapes(i).origin.x = 5. A new instance starts with numeric fields at 0, Boolean fields False, variable strings empty, and STRING * n fields padded to n spaces. TYPE variables can be written to and read from RANDOM and BINARY files with PUT and GET (see GET / PUT).
     
     .h2 ENUM...END ENUM
     
    @@ -775,7 +775,7 @@ If heading = Direction.West Then Print "Going west"
     
     The first member is 0 and each member without a value is one more than the member before it. A value may be any constant expression (see CONST), including members declared earlier in the same ENUM or in another one. Members are Long constants and can be used on their own (West) or with the ENUM name in front (Direction.West). If two ENUMs have a member with the same name, that name must be written with its ENUM name.
     
    -The ENUM name can be used as a type wherever a type is allowed: DIM, parameters, FUNCTION results and TYPE fields. Such a variable is a Long, so it can hold any whole number, not only the members. ENUMs are declared at module level (not inside a SUB or FUNCTION) and are visible everywhere in the program; declare an ENUM before the code that uses its members. PRIVATE ENUM keeps the ENUM and its members to the file it is declared in, like PRIVATE CONST; PUBLIC ENUM (the same as a plain ENUM) is seen everywhere. Members are constants: Direction.West = 3 is a compile error ("Cannot assign to a constant").
    +The ENUM name can be used as a type wherever a type is allowed: DIM, parameters, FUNCTION results, and TYPE fields. Such a variable is a Long, so it can hold any whole number, not only the members. ENUMs are declared at module level (not inside a SUB or FUNCTION) and are visible everywhere in the program; declare an ENUM before the code that uses its members. PRIVATE ENUM keeps the ENUM and its members to the file it is declared in, like PRIVATE CONST; PUBLIC ENUM (the same as a plain ENUM) is seen everywhere. Members are constants: Direction.West = 3 is a compile error ("Cannot assign to a constant").
     
     .h2 DECLARE
     
    @@ -808,7 +808,7 @@ End Declare
     
     Arguments are always passed by value. Each argument is converted to the type its parameter is declared with before the call, so a Double passed to a Long parameter arrives as a whole number and a Single parameter receives a Single. Declare Integer only for native values that fit in 16 bits; use Long for 32-bit values, or large results are cut down to 16 bits. A Byte parameter or result is 8 bits. A Currency parameter receives the amount as a 64-bit whole number of ten-thousandths; a DECLARE FUNCTION cannot return a Currency.
     
    -A String parameter or result declared AS STRING is text: it ends at the first CHR$(0). To pass or receive any bytes, CHR$(0) included (network data, sound samples, encrypted data, checksums of binary data), the declaration says BINARY STRING instead. To the program it is an ordinary String; LEN, MID$, ASC and the other string functions see every byte. BINARY STRING is allowed only in a DECLARE LIBRARY block, and the shipped include files already use it where a function works with bytes.
    +A String parameter or result declared AS STRING is text: it ends at the first CHR$(0). To pass or receive any bytes, CHR$(0) included (network data, sound samples, encrypted data, checksums of binary data), the declaration says BINARY STRING instead. To the program it is an ordinary String; LEN, MID$, ASC, and the other string functions see every byte. BINARY STRING is allowed only in a DECLARE LIBRARY block, and the shipped include files already use it where a function works with bytes.
     
     .code
     Declare Library "mylib"
    @@ -838,7 +838,7 @@ Sub Counter()
     End Sub
     .endcode
     
    -Several variables can be declared in one STATIC statement, separated by commas, each with its own AS clause; STRING * n works as in DIM. Arrays and TYPE variables can be STATIC too and keep their contents between calls: a fixed-size array (Static hits(1 To 10) As Long) or a dynamic one sized later with REDIM (Static names() As String). In a class module each object has its own copy of a procedure's STATIC variables, arrays and TYPE variables, as in Visual Basic: two objects counting calls count separately. A STATIC AS NEW object or WithEvents variable is not supported (a compile error); use a module-level or member variable for those.
    +Several variables can be declared in one STATIC statement, separated by commas, each with its own AS clause; STRING * n works as in DIM. Arrays and TYPE variables can be STATIC too and keep their contents between calls: a fixed-size array (Static hits(1 To 10) As Long) or a dynamic one sized later with REDIM (Static names() As String). In a class module each object has its own copy of a procedure's STATIC variables, arrays, and TYPE variables, as in Visual Basic: two objects counting calls count separately. A STATIC AS NEW object or WithEvents variable is not supported (a compile error); use a module-level or member variable for those.
     
     .code
     Sub Remember(ByVal word As String)
    @@ -905,7 +905,7 @@ LET variable = expression
     
     The LET keyword is optional and supported for compatibility.
     
    -Assigning a whole TYPE value to a TYPE variable, array element or field copies the value's fields into the record that is already there, as in Visual Basic: the two stay separate records (changing one later does not change the other), and a SUB that was passed one of the target's fields by reference sees the new value.
    +Assigning a whole TYPE value to a TYPE variable, array element, or field copies the value's fields into the record that is already there, as in Visual Basic: the two stay separate records (changing one later does not change the other), and a SUB that was passed one of the target's fields by reference sees the new value.
     
     .h2 SWAP
     
    @@ -956,7 +956,7 @@ LSet h = r              ' h.kind = "AB", h.size = 10
     
     .h2 SET
     
    -Stores an object reference -- a form, a control, an instance of a class module, a Collection, or Nothing -- in a variable, array element, TYPE field or property. After SET both names refer to the same object; nothing is copied.
    +Stores an object reference -- a form, a control, an instance of a class module, a Collection, or Nothing -- in a variable, array element, TYPE field, or property. After SET both names refer to the same object; nothing is copied.
     
     .code
     SET variable = objectExpression
    @@ -987,7 +987,7 @@ The value must be an object or Nothing; anything else raises error 424 (Object r
     
     .h2 ERASE
     
    -Frees the memory of a dynamic array (declared with DIM name(), created by REDIM, or an array parameter) and resets it to the undimensioned state; using it again before a REDIM raises error 13. A fixed array (declared with bounds) keeps its bounds and has every element reset to zero, an empty string, False or a fresh TYPE instance.
    +Frees the memory of a dynamic array (declared with DIM name(), created by REDIM, or an array parameter) and resets it to the undimensioned state; using it again before a REDIM raises error 13. A fixed array (declared with bounds) keeps its bounds and has every element reset to zero, an empty string, False, or a fresh TYPE instance.
     
     .code
     ERASE arrayName
    @@ -1234,9 +1234,9 @@ Sub Greet(ByVal name As String)
     End Sub
     .endcode
     
    -A SUB, FUNCTION or Property procedure stands on its own in the program: defining one inside another procedure, or inside an IF, FOR, DO, WHILE, SELECT CASE or WITH block, is a compile error.
    +A SUB, FUNCTION, or Property procedure stands on its own in the program: defining one inside another procedure, or inside an IF, FOR, DO, WHILE, SELECT CASE, or WITH block, is a compile error.
     
    -Parameters are passed by reference by default (nested calls pass the same variable on, so a SUB called from a SUB can still change the original); BYREF may be written out to say so, and BYVAL selects value semantics. A by-reference parameter is the caller's variable itself, as in Visual Basic: a write to it changes the variable at once (the caller's code, or another parameter naming the same variable, sees the new value straight away, even if the procedure later stops with an error), and what is stored is converted to the variable's own type (a STRING * n keeps its length). This holds for a plain variable, an array element (`bump a(3)` changes `a(3)` in place), a TYPE field (`bump rec.count`, `bump recs(2).count`, `bump rec.inner.count`) and, inside a class, one of the class's own member variables. While an array element (or a field of one) is passed by reference, its array cannot be given a new size or erased: REDIM of that array, or ERASE of it when it is a dynamic array, raises error 10 (This array is fixed or temporarily locked), as in Visual Basic (ERASE of a fixed-size array only clears its elements and is allowed). The array is free again as soon as the call returns. Assigning a whole TYPE value to the variable (or element) whose field is being passed copies the value into the same record, so the parameter sees the new value (as in Visual Basic). A parameter written as name() receives a whole array by reference; the caller passes `arr()` (or just `arr`), and the SUB may index, REDIM or ERASE it. Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:
    +Parameters are passed by reference by default (nested calls pass the same variable on, so a SUB called from a SUB can still change the original); BYREF may be written out to say so, and BYVAL selects value semantics. A by-reference parameter is the caller's variable itself, as in Visual Basic: a write to it changes the variable at once (the caller's code, or another parameter naming the same variable, sees the new value straight away, even if the procedure later stops with an error), and what is stored is converted to the variable's own type (a STRING * n keeps its length). This holds for a plain variable, an array element (`bump a(3)` changes `a(3)` in place), a TYPE field (`bump rec.count`, `bump recs(2).count`, `bump rec.inner.count`) and, inside a class, one of the class's own member variables. While an array element (or a field of one) is passed by reference, its array cannot be given a new size or erased: REDIM of that array, or ERASE of it when it is a dynamic array, raises error 10 (This array is fixed or temporarily locked), as in Visual Basic (ERASE of a fixed-size array only clears its elements and is allowed). The array is free again as soon as the call returns. Assigning a whole TYPE value to the variable (or element) whose field is being passed copies the value into the same record, so the parameter sees the new value (as in Visual Basic). A parameter written as name() receives a whole array by reference; the caller passes `arr()` (or just `arr`), and the SUB may index, REDIM, or ERASE it. Use EXIT SUB to return early. A SUB is called either with or without parentheses; when used as a statement, parentheses are optional:
     
     .code
     Greet "World"
    @@ -1246,7 +1246,7 @@ Call Greet("World")
     
     As in Visual Basic and QBasic, a single argument in parentheses after the name of a SUB called without CALL (Bump (n), or Bump(n)) is an expression in parentheses, so it is passed by value: the SUB gets a copy and n is not changed. Write Bump n, or Call Bump(n), to pass n by reference.
     
    -An argument is converted to the type of a typed parameter (one declared with AS, a type suffix or a DEFtype rule). A value -- a literal, an expression, a variable in its own parentheses, or anything given to a BYVAL parameter -- is converted the way an assignment would convert it (Report 3.7 gives an Integer parameter 4). A variable, array element or TYPE field given to a by-reference parameter must have the parameter's own type, as in Visual Basic: an Integer parameter cannot be the caller's Double variable ("ByRef argument type mismatch" when the program is compiled); pass CINT(d), or (d) in its own parentheses, to pass a converted copy. A variable declared without a type (a Variant) holds any value and is passed as it is. Through an object variable declared As Object the procedure is only found when the program runs, so a variable of another type is passed as a converted copy instead.
    +An argument is converted to the type of a typed parameter (one declared with AS, a type suffix, or a DEFtype rule). A value -- a literal, an expression, a variable in its own parentheses, or anything given to a BYVAL parameter -- is converted the way an assignment would convert it (Report 3.7 gives an Integer parameter 4). A variable, array element, or TYPE field given to a by-reference parameter must have the parameter's own type, as in Visual Basic: an Integer parameter cannot be the caller's Double variable ("ByRef argument type mismatch" when the program is compiled); pass CINT(d), or (d) in its own parentheses, to pass a converted copy. A variable declared without a type (a Variant) holds any value and is passed as it is. Through an object variable declared As Object the procedure is only found when the program runs, so a variable of another type is passed as a converted copy instead.
     
     .code
     Sub Fill(v() As Integer, ByRef count As Integer)
    @@ -1277,7 +1277,7 @@ Announce "hello"            ' loud defaults to 0
     Announce "hello", True      ' loud is -1
     .endcode
     
    -An OPTIONAL parameter can also name the value it takes when it is left out, after an equals sign: Optional count As Integer = 10. The default is a constant expression, like the value of a CONST: numbers, strings, dates, other constants and the built-in functions CONST allows, converted to the parameter's type (a value that does not fit is a compile error). A parameter with a default is never Missing -- ISMISSING is False for it -- whether it is left out at the end, skipped between commas, or the procedure is reached through an object. Defaults work for SUB and FUNCTION procedures and for the procedures and Property procedures of class modules, including calls written before the procedure itself. As in Visual Basic the default is the one the declaration being called gives: a call through an interface (a variable declared As the interface class) passes the interface's default, even when the implementing class declares another. An object parameter (As Object, As Control, As Form or As a class) can only have the default Nothing, as in Visual Basic: Optional target As Object = Nothing; a Variant parameter may have Nothing too. A default cannot be given to a required parameter, an array or a TYPE, or in DEF FN and DECLARE LIBRARY. A DECLARE SUB / FUNCTION line must say whether each parameter has a default, as its definition does.
    +An OPTIONAL parameter can also name the value it takes when it is left out, after an equals sign: Optional count As Integer = 10. The default is a constant expression, like the value of a CONST: numbers, strings, dates, other constants, and the built-in functions CONST allows, converted to the parameter's type (a value that does not fit is a compile error). A parameter with a default is never Missing -- ISMISSING is False for it -- whether it is left out at the end, skipped between commas, or the procedure is reached through an object. Defaults work for SUB and FUNCTION procedures and for the procedures and Property procedures of class modules, including calls written before the procedure itself. As in Visual Basic the default is the one the declaration being called gives: a call through an interface (a variable declared As the interface class) passes the interface's default, even when the implementing class declares another. An object parameter (As Object, As Control, As Form, or As a class) can only have the default Nothing, as in Visual Basic: Optional target As Object = Nothing; a Variant parameter may have Nothing too. A default cannot be given to a required parameter, an array, or a TYPE, or in DEF FN and DECLARE LIBRARY. A DECLARE SUB / FUNCTION line must say whether each parameter has a default, as its definition does.
     
     .code
     Const SIDE = 10
    @@ -1321,7 +1321,7 @@ Print Mid$(String:="hello", Start:=2)    ' ello
     MsgBox Prompt:="Saved", Title:="Notes"
     .endcode
     
    -Named arguments work for SUB and FUNCTION procedures, DEF FN functions, DECLARE LIBRARY procedures and the procedures of class modules (through a variable of the class, or through an Object or Variant variable), and for the built-in functions that Visual Basic gives argument names to: MsgBox (Prompt, Buttons, Title), InputBox (Prompt, Title, Default), Mid (String, Start, Length), Left and Right (String, Length), InStr (Start, String1, String2), InStrRev (StringCheck, StringMatch, Start, Compare), Replace (Expression, Find, Replace, Start, Count, Compare), Split (Expression, Delimiter, Limit, Compare), Join (SourceArray, Delimiter), Filter (SourceArray, Match, Include, Compare), StrComp (String1, String2, Compare), String (Number, Character), Format (Expression, Format), IIf (Expression, TruePart, FalsePart), DateAdd (Interval, Number, Date), DateDiff and DatePart (Interval, Date1 and Date2 or Date, FirstDayOfWeek, FirstWeekOfYear), DateSerial (Year, Month, Day), TimeSerial (Hour, Minute, Second), Weekday (Date, FirstDayOfWeek), WeekdayName (Weekday, Abbreviate, FirstDayOfWeek), MonthName (Month, Abbreviate), and a Collection's Add (Item, Key, Before, After), Item and Remove (Index).
    +Named arguments work for SUB and FUNCTION procedures, DEF FN functions, DECLARE LIBRARY procedures, and the procedures of class modules (through a variable of the class, or through an Object or Variant variable), and for the built-in functions that Visual Basic gives argument names to: MsgBox (Prompt, Buttons, Title), InputBox (Prompt, Title, Default), Mid (String, Start, Length), Left and Right (String, Length), InStr (Start, String1, String2), InStrRev (StringCheck, StringMatch, Start, Compare), Replace (Expression, Find, Replace, Start, Count, Compare), Split (Expression, Delimiter, Limit, Compare), Join (SourceArray, Delimiter), Filter (SourceArray, Match, Include, Compare), StrComp (String1, String2, Compare), String (Number, Character), Format (Expression, Format), IIf (Expression, TruePart, FalsePart), DateAdd (Interval, Number, Date), DateDiff and DatePart (Interval, Date1 and Date2 or Date, FirstDayOfWeek, FirstWeekOfYear), DateSerial (Year, Month, Day), TimeSerial (Hour, Minute, Second), Weekday (Date, FirstDayOfWeek), WeekdayName (Weekday, Abbreviate, FirstDayOfWeek), MonthName (Month, Abbreviate), and a Collection's Add (Item, Key, Before, After), Item, and Remove (Index).
     
     A name the procedure does not have is a compile error (Named argument not found), and so is a parameter given twice. When the procedure is only found as the program runs (a method called through an Object or Variant variable) the same mistakes raise error 448 (Named argument not found) and error 450 (Wrong number of arguments). The methods of forms and controls do not take named arguments (error 446, Object doesn't support named arguments), and neither does a ParamArray parameter.
     
    @@ -1352,7 +1352,7 @@ Function Sum(ParamArray n() As Double) As Double
     End Function
     .endcode
     
    -Without an AS clause (and no type suffix or DEFtype rule for the name) the ParamArray is an array of Variants (TYPENAME gives "Variant()"): each element holds the argument that was passed with its own type, so numbers, strings and objects can be mixed, and FOR EACH can walk it. With an AS clause every argument is converted to that type. An argument that is a variable, a whole array, an array element (a(i), m(i, j)), a TYPE field (rec.f, arr(i).f, rec.inner.f) or, inside a class, one of the class's own member variables is passed by reference, as in Visual Basic: the element of the ParamArray is that variable, so a write to it changes the variable at once, converted to the variable's type (a value that cannot be converted raises its error at the assignment), and passing the element on (Bump v(0)) passes the variable. With an AS clause an argument is passed by reference only when its own type is that type; others are converted and passed by value. The same holds for a class's methods, whether the object variable is declared as the class or As Object / As Variant. While the call runs, the arrays whose elements were given are locked: REDIM of one of them (or ERASE of a dynamic one) raises error 10, as for any array element passed by reference. Copying the ParamArray (keep = v) copies the values, not the variables. Expressions and object properties are passed by value. Leaving an argument out (Sum 1, , 3) gives a Missing element. A PARAMARRAY cannot be combined with OPTIONAL parameters, cannot be BYVAL or BYREF, cannot hold a TYPE, and takes at most 31 arguments. It is not available in DEF FN or DECLARE LIBRARY.
    +Without an AS clause (and no type suffix or DEFtype rule for the name) the ParamArray is an array of Variants (TYPENAME gives "Variant()"): each element holds the argument that was passed with its own type, so numbers, strings, and objects can be mixed, and FOR EACH can walk it. With an AS clause every argument is converted to that type. An argument that is a variable, a whole array, an array element (a(i), m(i, j)), a TYPE field (rec.f, arr(i).f, rec.inner.f) or, inside a class, one of the class's own member variables is passed by reference, as in Visual Basic: the element of the ParamArray is that variable, so a write to it changes the variable at once, converted to the variable's type (a value that cannot be converted raises its error at the assignment), and passing the element on (Bump v(0)) passes the variable. With an AS clause an argument is passed by reference only when its own type is that type; others are converted and passed by value. The same holds for a class's methods, whether the object variable is declared as the class or As Object / As Variant. While the call runs, the arrays whose elements were given are locked: REDIM of one of them (or ERASE of a dynamic one) raises error 10, as for any array element passed by reference. Copying the ParamArray (keep = v) copies the values, not the variables. Expressions and object properties are passed by value. Leaving an argument out (Sum 1, , 3) gives a Missing element. A PARAMARRAY cannot be combined with OPTIONAL parameters, cannot be BYVAL or BYREF, cannot hold a TYPE, and takes at most 31 arguments. It is not available in DEF FN or DECLARE LIBRARY.
     
     .h2 FUNCTION...END FUNCTION
     
    @@ -1373,7 +1373,7 @@ Function Square(ByVal n As Double) As Double
     End Function
     .endcode
     
    -Assign to the function name to set the return value. The value is converted to the function's type (AS clause, type suffix or DEFtype), so FUNCTION F AS INTEGER with F = 2.5 returns 2. Inside the function its name on its own reads the value set so far (Total = Total + n), as in Visual Basic; with an argument list -- F() or F(n - 1) -- it calls the function again. Use EXIT FUNCTION to return early.
    +Assign to the function name to set the return value. The value is converted to the function's type (AS clause, type suffix, or DEFtype), so FUNCTION F AS INTEGER with F = 2.5 returns 2. Inside the function its name on its own reads the value set so far (Total = Total + n), as in Visual Basic; with an argument list -- F() or F(n - 1) -- it calls the function again. Use EXIT FUNCTION to return early.
     
     A FUNCTION can also be called as a statement, with or without parentheses; its result is discarded:
     
    @@ -1387,7 +1387,7 @@ Beep2 3              ' result discarded
     
     name(...) in an expression calls a FUNCTION; using a SUB there is a compile error.
     
    -.h3 Labels, GOSUB and Variables in Procedures
    +.h3 Labels, GOSUB, and Variables in Procedures
     
     Labels belong to the SUB or FUNCTION they are written in (or to the module level). GOTO, GOSUB, ON ... GOTO/GOSUB and ON ERROR GOTO can only reach a label of the same procedure, and different procedures can reuse the same label name. GOSUB and RETURN work inside a procedure; see GOTO / GOSUB / RETURN. A procedure sees module variables only when they are SHARED; see Variable Scope and SHARED.
     
    @@ -1434,7 +1434,7 @@ EXIT FUNCTION
     EXIT PROPERTY
     .endcode
     
    -EXIT PROPERTY leaves a Property Get, Let or Set procedure of a class module (see Objects and Class Modules).
    +EXIT PROPERTY leaves a Property Get, Let, or Set procedure of a class module (see Objects and Class Modules).
     
     .h2 CALL
     
    @@ -1457,7 +1457,7 @@ RETURN
     
     A label is a name followed by a colon at the start of a line, or a line number at the start of a line (see Line Numbers below). Labels belong to the SUB or FUNCTION they are in (module-level labels to the module level): a jump can only reach a label of the same procedure, the same label name can be used in several procedures, and a label may have the same name as a variable. A jump to a label that is not defined in the procedure, or to a SUB or variable name, is a compile error.
     
    -GOSUB records where it was called from, runs the code at the label, and RETURN goes back to the statement after the GOSUB. This works at module level and inside a SUB or FUNCTION alike. Each procedure call has its own set of pending GOSUBs: RETURN only returns from a GOSUB made in the same procedure call, and leaving the procedure (END SUB, EXIT SUB, EXIT FUNCTION or an error handled in a caller) discards the ones still pending. RETURN with no pending GOSUB raises error 3 (RETURN without GOSUB). To leave a SUB or FUNCTION, use EXIT SUB / EXIT FUNCTION.
    +GOSUB records where it was called from, runs the code at the label, and RETURN goes back to the statement after the GOSUB. This works at module level and inside a SUB or FUNCTION alike. Each procedure call has its own set of pending GOSUBs: RETURN only returns from a GOSUB made in the same procedure call, and leaving the procedure (END SUB, EXIT SUB, EXIT FUNCTION, or an error handled in a caller) discards the ones still pending. RETURN with no pending GOSUB raises error 3 (RETURN without GOSUB). To leave a SUB or FUNCTION, use EXIT SUB / EXIT FUNCTION.
     
     .code
     GoSub Initialize
    @@ -1494,7 +1494,7 @@ If the expression evaluates to 1, control goes to the first label; 2, the second
     
     .h2 Line Numbers
     
    -As in QBasic, a line can start with a line number, a whole number from 0 to 65529. A line number is a label: GOTO, GOSUB, ON ... GOTO / GOSUB, ON ERROR GOTO, RESUME and RESTORE can name it, it belongs to its procedure like any other label, and the same number cannot start two lines of a procedure. Numbers need not be in order, lines without a number are fine, and the number is read as a value, so 010 and 10 name the same line. IF can branch to a line number directly: IF x THEN 100 ELSE 200 is IF x THEN GOTO 100 ELSE GOTO 200, and IF x GOTO 100 works too.
    +As in QBasic, a line can start with a line number, a whole number from 0 to 65529. A line number is a label: GOTO, GOSUB, ON ... GOTO / GOSUB, ON ERROR GOTO, RESUME, and RESTORE can name it, it belongs to its procedure like any other label, and the same number cannot start two lines of a procedure. Numbers need not be in order, lines without a number are fine, and the number is read as a value, so 010 and 10 name the same line. IF can branch to a line number directly: IF x THEN 100 ELSE 200 is IF x THEN GOTO 100 ELSE GOTO 200, and IF x GOTO 100 works too.
     
     .code
     10 INPUT "Guess"; g
    @@ -1556,7 +1556,7 @@ Print #1, "Written to file"
     
     .h2 LPRINT
     
    -Prints on the printer instead of the screen. Everything PRINT accepts works the same way: ; and , between items, SPC, TAB and LPRINT USING. The printer keeps its own column for TAB and the 14-column zones.
    +Prints on the printer instead of the screen. Everything PRINT accepts works the same way: ; and , between items, SPC, TAB, and LPRINT USING. The printer keeps its own column for TAB and the 14-column zones.
     
     .code
     LPRINT [expression [{; | ,} expression] ...]
    @@ -1571,7 +1571,7 @@ LPrint Using "Total: $$####.##"; total
     LPrint Chr$(12)                 ' eject the page
     .endcode
     
    -For a whole document with margins, a header, a footer and page numbers, and for the Print dialog, use the Printing Library (print.bas, see Include Libraries).
    +For a whole document with margins, a header, a footer, and page numbers, and for the Print dialog, use the Printing Library (print.bas, see Include Libraries).
     
     .h3 PRINT USING
     
    @@ -1625,7 +1625,7 @@ Input "Enter your name: "; name$
     Input #1, line$
     .endcode
     
    -The target may be a variable, an array element or a TYPE field (INPUT, INPUT #, LINE INPUT # and READ alike). INPUT and INPUT # read a number into a numeric target the way VAL does (12abc gives 12).
    +The target may be a variable, an array element, or a TYPE field (INPUT, INPUT #, LINE INPUT #, and READ alike). INPUT and INPUT # read a number into a numeric target the way VAL does (12abc gives 12).
     
     .h2 DATA / READ / RESTORE
     
    @@ -1637,7 +1637,7 @@ READ variable1, variable2, ...
     RESTORE [label]
     .endcode
     
    -DATA statements define a pool of values. READ reads the next value from the pool into a variable, array element or TYPE field. RESTORE resets the read pointer to the beginning; RESTORE label (or a line number) makes the next READ take the first DATA item at or after that label. An item in quotes is a string; an unquoted item is taken as raw text up to the next comma, colon, comment or end of line (so keywords, several words and text such as 12abc are read as they are written) and converted when READ into a numeric variable, which raises error 13 (Type mismatch) when the item is not a number. The most negative Integer and Long values (-32768, -2147483648) are accepted.
    +DATA statements define a pool of values. READ reads the next value from the pool into a variable, array element, or TYPE field. RESTORE resets the read pointer to the beginning; RESTORE label (or a line number) makes the next READ take the first DATA item at or after that label. An item in quotes is a string; an unquoted item is taken as raw text up to the next comma, colon, comment, or end of line (so keywords, several words, and text such as 12abc are read as they are written) and converted when READ into a numeric variable, which raises error 13 (Type mismatch) when the item is not a number. The most negative Integer and Long values (-32768, -2147483648) are accepted.
     
     .code
     Data 10, 20, 30, "Hello"
    @@ -1695,7 +1695,7 @@ Each SUB and FUNCTION has its own ON ERROR handler:
     .item The label named by ON ERROR GOTO must be in the same procedure.
     .endlist
     
    -An error that no handler traps ends the program, as in Visual Basic, wherever it happens: in module-level code or in an event handler (a Click, a Timer, a form's Load ...). DVX shows it in a BASIC Runtime Error box -- the error number, the line and the message -- and writes it to the DVX log; the program's windows close once the box is dismissed. In the IDE the Output window shows the error and the editor goes to its line.
    +An error that no handler traps ends the program, as in Visual Basic, wherever it happens: in module-level code or in an event handler (a Click, a Timer, a form's Load ...). DVX shows it in a BASIC Runtime Error box -- the error number, the line, and the message -- and writes it to the DVX log; the program's windows close once the box is dismissed. In the IDE the Output window shows the error and the editor goes to its line.
     
     ON ERROR RESUME NEXT, as in Visual Basic, needs no handler: a statement that fails is abandoned and the program goes on at the next one, with ERR (Err.Number) and Err.Description telling what went wrong. An error in a procedure that has no handler of its own leaves that procedure, and the program goes on after the statement that called it. The statement clears ERR, and so does the end of the procedure that used it; check ERR right after the statements that may fail:
     
    @@ -1721,7 +1721,7 @@ Err holds the current error the way ERR and ERROR do:
       Err.Clear                                 Clears the error: ERR becomes 0, the description and source ""
     .endtable
     
    -ERROR$(n) (or ERROR(n)) gives the standard message of error number n, "User-defined error" for a number without one and "" for 0; without an argument, the current error's message.
    +ERROR$(n) (or ERROR(n)) gives the standard message of error number n, "User-defined error" for a number without one, and "" for 0; without an argument, the current error's message.
     
     .code
     On Error GoTo Failed
    @@ -1732,7 +1732,7 @@ Failed:
         Resume Next
     .endcode
     
    -RESUME NEXT continues after the failing statement. When the error happened in the condition of an IF, ELSEIF, WHILE, DO or LOOP, the condition counts as false (the block is skipped, the loop is left); after an error in a FOR statement the loop is skipped; after an error in the SELECT CASE expression the whole SELECT CASE block is skipped; after an error in a CASE test that test counts as false (the next value or CASE is tried); after an error in an ON ... GOTO/GOSUB selector no branch is taken and execution continues with the next statement.
    +RESUME NEXT continues after the failing statement. When the error happened in the condition of an IF, ELSEIF, WHILE, DO, or LOOP, the condition counts as false (the block is skipped, the loop is left); after an error in a FOR statement the loop is skipped; after an error in the SELECT CASE expression the whole SELECT CASE block is skipped; after an error in a CASE test that test counts as false (the next value or CASE is tried); after an error in an ON ... GOTO/GOSUB selector no branch is taken and execution continues with the next statement.
     
     .code
     On Error GoTo ErrorHandler
    @@ -1753,7 +1753,7 @@ ErrorHandler:
       3        RETURN without GOSUB
       4        Out of DATA
       5        Illegal function call (bad argument to a built-in function; an undefined math result such as a negative number to a fractional power)
    -  6        Overflow (value does not fit the target type; EXP, ^ or other math leaving the numeric range; a file size or position past 2 GB in LOF, FILELEN, LOC, SEEK, GET or PUT)
    +  6        Overflow (value does not fit the target type; EXP, ^, or other math leaving the numeric range; a file size or position past 2 GB in LOF, FILELEN, LOC, SEEK, GET, or PUT)
       7        Out of memory
       9        Subscript out of range / invalid variable or field index
       10       This array is fixed or temporarily locked (REDIM, or ERASE of a dynamic array, while one of its elements, or a field of one, is passed by reference to a procedure that is still running)
    @@ -1770,7 +1770,7 @@ ErrorHandler:
       57       Device I/O error (printing failed or was cancelled)
       58       File already exists or rename failed
       59       Bad record length (OPEN ... LEN must be 1 to 32767; GET/PUT data longer than LEN)
    -  63       Bad record number (SEEK, GET or PUT position below 1)
    +  63       Bad record number (SEEK, GET, or PUT position below 1)
       64       Bad file name (WIDTH of a device DVX does not have)
       67       Too many files open
       68       Device unavailable (no printer to print on)
    @@ -1779,7 +1779,7 @@ ErrorHandler:
       91       Object variable not set (property, method, Show/Hide/Unload on a value that is not a form or control, or on Nothing)
       93       Invalid pattern string (a LIKE pattern with a [ that has no ])
       94       Invalid use of Null (Null where a real number or text is needed; see Variants)
    -  424      Object required (SET, Is or TypeOf with a value that is not an object)
    +  424      Object required (SET, Is, or TypeOf with a value that is not an object)
       438      Object doesn't support this property or method (the object has no member of that name, or it is Private)
       446      Object doesn't support named arguments (a form or control method called with Name:=value)
       448      Named argument not found (a Name:= the method does not have; a left-out OPTIONAL Variant argument, Missing, used as a value)
    @@ -1835,8 +1835,8 @@ END
     .endcode
     
     .topic lang.sound
    -.title Sound, Joysticks and Frames
    -.toc 2 Sound, Joysticks and Frames
    +.title Sound, Joysticks, and Frames
    +.toc 2 Sound, Joysticks, and Frames
     .index BEEP
     .index SOUND
     .index PLAY
    @@ -1849,9 +1849,9 @@ END
     .index FRAMEWAIT
     .index Frame rate
     
    -.h1 Sound, Joysticks and Frames
    +.h1 Sound, Joysticks, and Frames
     
    -These statements work as they do in QBasic, on whatever sound hardware the computer has: a sound card, the PC speaker, or none at all, in which case the program runs the same but silently. Several programs can make sound at once. For sound files, tones of any shape and music files, see the sound.bas and music.bas include libraries; for more joystick detail, joystick.bas.
    +These statements work as they do in QBasic, on whatever sound hardware the computer has: a sound card, the PC speaker, or none at all, in which case the program runs the same but silently. Several programs can make sound at once. For sound files, tones of any shape, and music files, see the sound.bas and music.bas include libraries; for more joystick detail, joystick.bas.
     
     .h2 BEEP
     
    @@ -1894,7 +1894,7 @@ PLAY "T160 O3 L8 E D C D E E E4"
       MN          Music normal: each note sounds for 7/8 of its length (the default).
       ML          Music legato: notes sound for their whole length.
       MS          Music staccato: notes sound for 3/4 of their length.
    -  MF          Music foreground: PLAY, SOUND and BEEP wait until the sound has finished (the default).
    +  MF          Music foreground: PLAY, SOUND, and BEEP wait until the sound has finished (the default).
       MB          Music background: the program goes on while the music plays; it only waits when more than 32 notes are waiting.
       I n         DVX's own: play the notes that follow on General MIDI instrument n (1 to 128; 1 is the piano, 41 the violin, 74 the flute) through the music synthesizer. I0 goes back to plain tones (the default).
       X           "X" + VARPTR$(s$) plays the commands in s$ at that point (s$ may use X itself, up to 16 levels deep).
    @@ -1926,7 +1926,7 @@ If PLAY(0) < 4 Then PLAY "MB " + tune$
     
     .h2 STICK and STRIG
     
    -STICK(n) returns where a joystick points, from 1 (all the way left or up) to 200 (all the way right or down), about 100 in the middle, or 0 when there is no such joystick. STICK(0) reads joystick A's X and remembers A's Y and joystick B's X and Y, which STICK(1), STICK(2) and STICK(3) then return; so call STICK(0) first.
    +STICK(n) returns where a joystick points, from 1 (all the way left or up) to 200 (all the way right or down), about 100 in the middle, or 0 when there is no such joystick. STICK(0) reads joystick A's X and remembers A's Y and joystick B's X and Y, which STICK(1), STICK(2), and STICK(3) then return; so call STICK(0) first.
     
     STRIG(n) returns True (-1) or False (0) for the buttons:
     
    @@ -1943,7 +1943,7 @@ STRIG(n) returns True (-1) or False (0) for the buttons:
       7     joystick B's second button is down
     .endtable
     
    -Joystick A is the first joystick; joystick B the second one, or the second stick of a joystick with four axes. When no joystick is connected the keyboard stands in for joystick A (see the Control Panel's Game Controllers tab). STRIG ON, STRIG OFF and ON STRIG are not supported; test STRIG in a Timer or a loop instead.
    +Joystick A is the first joystick; joystick B the second one, or the second stick of a joystick with four axes. When no joystick is connected the keyboard stands in for joystick A (see the Control Panel's Game Controllers tab). STRIG ON, STRIG OFF, and ON STRIG are not supported; test STRIG in a Timer or a loop instead.
     
     .code
     x = STICK(0)
    @@ -2005,7 +2005,7 @@ Loop Until quitting
     
     These statements work as they do in QBasic. The first one a program uses gives it a screen of its own: a window, titled with the program's name, the size of the screen mode. From then on PRINT and INPUT use that screen too (before it, PRINT writes to the output window and INPUT asks in a dialog box, as always). Keys typed in the window go to INKEY$ and INPUT. The window stays open when the program's code has finished, so its output can be read; closing it ends the program.
     
    -The names are not reserved: a program that has its own variable, SUB or constant called Color, Cls, Draw and so on keeps using it.
    +The names are not reserved: a program that has its own variable, SUB, or constant called Color, Cls, Draw, and so on keeps using it.
     
     .h2 Screen Modes
     
    @@ -2026,7 +2026,7 @@ The names are not reserved: a program that has its own variable, SUB or constant
     
     A program starts in SCREEN 0. SCREEN with a new mode (and WIDTH) clears the screen and restores the mode's colours: the 16 standard colours, and in SCREEN 13 the standard 256-colour palette. SCREEN with the mode the program is already in only changes the pages, so a program can flip pages in a loop. Other mode numbers, and graphics statements in SCREEN 0, are error 5 (Illegal function call). The colour switch argument is accepted and ignored.
     
    -Text uses 16-pixel-high characters in SCREEN 0 (25 rows), 11 and 12 (30 rows), 14-pixel ones in SCREEN 9 and 10 (25 rows) and 8-pixel ones everywhere else. The window shows each mode in the shape it has on a monitor: the 640 x 200 modes with every line doubled, 40-column text with every column doubled; SCREEN 9 and 10 show at 640 x 350 as they are. On the whole screen the 320 x 200 modes use the computer's 320 x 200 mode and the others the nearest mode, centred.
    +Text uses 16-pixel-high characters in SCREEN 0 (25 rows), 11 and 12 (30 rows), 14-pixel ones in SCREEN 9 and 10 (25 rows), and 8-pixel ones everywhere else. The window shows each mode in the shape it has on a monitor: the 640 x 200 modes with every line doubled, 40-column text with every column doubled; SCREEN 9 and 10 show at 640 x 350 as they are. On the whole screen the 320 x 200 modes use the computer's 320 x 200 mode and the others the nearest mode, centred.
     
     .code
     SCREEN mode [, [colorswitch] [, [activepage] [, visualpage]]]
    @@ -2074,7 +2074,7 @@ code = SCREEN(row, column [, colorflag])
     .item CLS clears the screen and puts the cursor in the top-left corner (the top of the text viewport). In a graphics mode with a VIEW set, CLS (or CLS 1) clears only the view; with a VIEW PRINT set, CLS (or CLS 2) clears only the text viewport; CLS 0 clears everything.
     .item LOCATE moves the cursor (rows and columns count from 1). cursor 1 shows the text cursor in SCREEN 0, 0 hides it (the default). QBasic's cursor size arguments are accepted and ignored.
     .item COLOR in SCREEN 0 takes a foreground 0 to 31 and a background 0 to 15. Foregrounds 16 to 31 blink (colour minus 16); as on a VGA, backgrounds 8 to 15 show as 0 to 7 and make the text blink too.
    -.item COLOR in SCREEN 1 takes a background colour (0 to 15) and a palette: an even number gives green, red and brown for colours 1 to 3, an odd one (the default) cyan, magenta and white. SCREEN 2 has no COLOR statement. In SCREEN 7 to 10 COLOR takes a foreground and a background colour (the colour of attribute 0: 0 to 15 in SCREEN 7 and 8, an EGA colour 0 to 63 in SCREEN 9, a shade 0 to 8 in SCREEN 10). In SCREEN 11 to 13 it takes only a foreground. The foreground is also the default colour of the drawing statements.
    +.item COLOR in SCREEN 1 takes a background colour (0 to 15) and a palette: an even number gives green, red, and brown for colours 1 to 3, an odd one (the default) cyan, magenta, and white. SCREEN 2 has no COLOR statement. In SCREEN 7 to 10 COLOR takes a foreground and a background colour (the colour of attribute 0: 0 to 15 in SCREEN 7 and 8, an EGA colour 0 to 63 in SCREEN 9, a shade 0 to 8 in SCREEN 10). In SCREEN 11 to 13 it takes only a foreground. The foreground is also the default colour of the drawing statements.
     .item CSRLIN is the cursor's row and POS(0) its column. Before the program has a screen, POS(0) is the output window's column.
     .item SCREEN(row, column) is the character code shown there; with a colorflag of 1, its colour (in SCREEN 0, foreground + 16 * background).
     .item PRINT scrolls the screen (or the text viewport) up at the bottom. CHR$(13) and CHR$(10) start a new line and CHR$(9) moves to the next multiple of 8 columns.
    @@ -2090,7 +2090,7 @@ VIEW PRINT [top TO bottom]
     
     .h2 INKEY$
     
    -Returns the next key typed in the program's window without waiting: "" when there is none, one character for a character key, and CHR$(0) + CHR$(scan code) for other keys (arrows, function keys). Keys held with Alt, and function keys held with Shift, Ctrl or Alt, give the codes QBasic gives: Alt+A is CHR$(0) + CHR$(30), Alt+1 CHR$(0) + CHR$(120), Shift+F1 CHR$(0) + CHR$(84), Ctrl+F1 CHR$(0) + CHR$(94) and Alt+F1 CHR$(0) + CHR$(104). Keys wait for INKEY$ in a queue of 64.
    +Returns the next key typed in the program's window without waiting: "" when there is none, one character for a character key, and CHR$(0) + CHR$(scan code) for other keys (arrows, function keys). Keys held with Alt, and function keys held with Shift, Ctrl, or Alt, give the codes QBasic gives: Alt+A is CHR$(0) + CHR$(30), Alt+1 CHR$(0) + CHR$(120), Shift+F1 CHR$(0) + CHR$(84), Ctrl+F1 CHR$(0) + CHR$(94), and Alt+F1 CHR$(0) + CHR$(104). Keys wait for INKEY$ in a queue of 64.
     
     .code
     Do
    @@ -2126,7 +2126,7 @@ Loop While InKey$ = ""
     
     .h2 INPUT and LINE INPUT on the Screen
     
    -On the program's screen INPUT shows its prompt at the cursor and reads what is typed there: Backspace takes back a character, Esc clears the line and Enter ends it. LINE INPUT reads a whole line, commas included, without adding "? " to the prompt. Without the program's screen LINE INPUT asks in a dialog box, like INPUT.
    +On the program's screen INPUT shows its prompt at the cursor and reads what is typed there: Backspace takes back a character, Esc clears the line, and Enter ends it. LINE INPUT reads a whole line, commas included, without adding "? " to the prompt. Without the program's screen LINE INPUT asks in a dialog box, like INPUT.
     
     .code
     LINE INPUT ["prompt";] variable$
    @@ -2134,7 +2134,7 @@ LINE INPUT ["prompt";] variable$
     
     .h2 Graphics Coordinates
     
    -Points are (x, y) in pixels from the top-left corner. STEP(x, y) is relative to the last point drawn (the centre of the screen at first). VIEW makes a smaller part of the screen the drawing area (coordinates start at its corner, and drawing stays inside it); WINDOW gives it coordinates of your own. Colour numbers are attributes: 0 to 3 in SCREEN 1 and 10, 0 and 1 in SCREEN 2 and 11, 0 to 15 in SCREEN 7, 8, 9 and 12, 0 to 255 in SCREEN 13; anything else is error 5.
    +Points are (x, y) in pixels from the top-left corner. STEP(x, y) is relative to the last point drawn (the centre of the screen at first). VIEW makes a smaller part of the screen the drawing area (coordinates start at its corner, and drawing stays inside it); WINDOW gives it coordinates of your own. Colour numbers are attributes: 0 to 3 in SCREEN 1 and 10, 0 and 1 in SCREEN 2 and 11, 0 to 15 in SCREEN 7, 8, 9, and 12, 0 to 255 in SCREEN 13; anything else is error 5.
     
     .code
     VIEW [[SCREEN] (x1, y1)-(x2, y2) [, [fill] [, border]]]
    @@ -2145,7 +2145,7 @@ value = PMAP(coordinate, n)
     .list
     .item VIEW with coordinates of its own sets the view; fill fills it and border draws a frame around it. VIEW SCREEN keeps coordinates counting from the screen's corner. VIEW alone is the whole screen again.
     .item WINDOW (x1, y1)-(x2, y2) maps the view to those coordinates with y going up; WINDOW SCREEN keeps y going down. WINDOW alone goes back to pixels.
    -.item PMAP converts between WINDOW coordinates and pixels of the view: n = 0 gives the pixel column of an x, 1 the pixel row of a y, 2 the x of a pixel column and 3 the y of a pixel row. Without a WINDOW both are pixels.
    +.item PMAP converts between WINDOW coordinates and pixels of the view: n = 0 gives the pixel column of an x, 1 the pixel row of a y, 2 the x of a pixel column, and 3 the y of a pixel row. Without a WINDOW both are pixels.
     .endlist
     
     .h2 PSET, PRESET, POINT
    @@ -2165,7 +2165,7 @@ PSET draws a point in the given colour or the COLOR foreground, PRESET in the gi
     LINE [[STEP](x1, y1)]-[STEP](x2, y2) [, [color] [, [B | BF] [, style]]]
     .endcode
     
    -Draws a line, a box (B) or a filled box (BF). Without a start point the line starts at the last point. style is a 16-bit pattern: each set bit (from the top) draws a pixel, each clear one skips it.
    +Draws a line, a box (B), or a filled box (BF). Without a start point the line starts at the last point. style is a 16-bit pattern: each set bit (from the top) draws a pixel, each clear one skips it.
     
     .code
     LINE (10, 10)-(100, 50), 4, BF
    @@ -2179,7 +2179,7 @@ LINE (0, 100)-(319, 100), 7, , &HF0F0
     CIRCLE [STEP](x, y), radius [, [color] [, [start] [, [end] [, aspect]]]]
     .endcode
     
    -Draws a circle, an ellipse or an arc. start and end are angles in radians (0 to 2 * pi, counterclockwise from the right); a negative angle also draws the radius to that end, so CIRCLE can draw pie slices. aspect is the height divided by the width; the default makes circles round on a 4:3 monitor (0.83 in SCREEN 13, 1 in SCREEN 12), so in SCREEN 13 a circle in the window looks slightly flat and a round one needs aspect 1.
    +Draws a circle, an ellipse, or an arc. start and end are angles in radians (0 to 2 * pi, counterclockwise from the right); a negative angle also draws the radius to that end, so CIRCLE can draw pie slices. aspect is the height divided by the width; the default makes circles round on a 4:3 monitor (0.83 in SCREEN 13, 1 in SCREEN 12), so in SCREEN 13 a circle in the window looks slightly flat and a round one needs aspect 1.
     
     .code
     CIRCLE (160, 100), 50, 14
    @@ -2196,7 +2196,7 @@ PAINT [STEP](x, y), tile$ [, [border] [, background$]]
     
     Fills the area around the point with color, up to pixels of the border colour (by default the fill colour itself).
     
    -With a string instead of a colour, PAINT fills the area with a pattern, as in QBasic. Each row of the pattern is one byte for each colour plane of the mode, and each bit is a pixel (the first bit is the left pixel): in SCREEN 2 and 11 one byte is eight pixels, colour 0 or 1; in SCREEN 7, 8, 9 and 12 four bytes (the blue, green, red and bright planes) make eight pixels of 16 colours; in SCREEN 10 two bytes. In SCREEN 1 each byte is four pixels of two bits, and in SCREEN 13 each byte is one pixel. A pattern has up to 64 rows and repeats over the screen. The fill stops at the border colour (by default the COLOR foreground) and covers the whole area once, even where it already has the pattern's colours. background$ is accepted as in QBasic: a pattern with more than two rows in a row equal to it is error 5.
    +With a string instead of a colour, PAINT fills the area with a pattern, as in QBasic. Each row of the pattern is one byte for each colour plane of the mode, and each bit is a pixel (the first bit is the left pixel): in SCREEN 2 and 11 one byte is eight pixels, colour 0 or 1; in SCREEN 7, 8, 9, and 12 four bytes (the blue, green, red, and bright planes) make eight pixels of 16 colours; in SCREEN 10 two bytes. In SCREEN 1 each byte is four pixels of two bits, and in SCREEN 13 each byte is one pixel. A pattern has up to 64 rows and repeats over the screen. The fill stops at the border colour (by default the COLOR foreground) and covers the whole area once, even where it already has the pattern's colours. background$ is accepted as in QBasic: a pattern with more than two rows in a row equal to it is error 5.
     
     .code
     SCREEN 12
    @@ -2229,7 +2229,7 @@ Draws with a small command language. Letters may be upper or lower case; spaces
       P c,b       PAINT at the current point with colour c up to border colour b.
     .endtable
     
    -The pen starts at the last point and stays where it ends. The colour, scale and turn stay from one DRAW to the next. A mistake in the string is error 5.
    +The pen starts at the last point and stays where it ends. The colour, scale, and turn stay from one DRAW to the next. A mistake in the string is error 5.
     
     As in QBasic, X runs another DRAW string and "=" takes a number, both given with VARPTR$: DRAW "X" + VARPTR$(shape$) and DRAW "C=" + VARPTR$(c) + "R=" + VARPTR$(n). DVX BASIC has no memory addresses, so VARPTR$ packs the value itself into its result (taken when VARPTR$ runs), and it takes any expression. Building the string with + and STR$ works too, such as DRAW "R" + STR$(n).
     
    @@ -2249,7 +2249,7 @@ PALETTE [attribute, color]
     PALETTE USING array[(first)]
     .endcode
     
    -Changes the colour an attribute shows. In SCREEN 11, 12 and 13, color is blue * 65536 + green * 256 + red with each part 0 to 63; in SCREEN 0 and 9 it is an EGA colour number 0 to 63; in SCREEN 1, 2, 7 and 8 one of the 16 standard colours; in SCREEN 10 a shade 0 to 8 (0 off, 4 grey, 8 bright, and 1 to 3, 5 to 7 blinking between two of them). PALETTE alone restores the mode's colours. PALETTE USING sets every attribute of the mode (256 in SCREEN 13) from a one-dimensional LONG array starting at element first (or its first element); -1 leaves that attribute as it is. On the whole screen a palette change shows at once, like on a VGA card; in the window, whatever is already drawn in that attribute changes colour too.
    +Changes the colour an attribute shows. In SCREEN 11, 12, and 13, color is blue * 65536 + green * 256 + red with each part 0 to 63; in SCREEN 0 and 9 it is an EGA colour number 0 to 63; in SCREEN 1, 2, 7, and 8 one of the 16 standard colours; in SCREEN 10 a shade 0 to 8 (0 off, 4 grey, 8 bright, and 1 to 3, 5 to 7 blinking between two of them). PALETTE alone restores the mode's colours. PALETTE USING sets every attribute of the mode (256 in SCREEN 13) from a one-dimensional LONG array starting at element first (or its first element); -1 leaves that attribute as it is. On the whole screen a palette change shows at once, like on a VGA card; in the window, whatever is already drawn in that attribute changes colour too.
     
     .code
     SCREEN 13
    @@ -2349,7 +2349,7 @@ CLOSE with a list closes each channel in it (the # is optional); CLOSE alone, an
     
     .h2 PRINT #
     
    -Writes text to a file, formatted exactly as PRINT shows it: numbers are followed by a space, a comma moves to the next 14-column zone, a semicolon keeps the line going, and TAB(n), SPC(n) and PRINT #channel, USING work as they do for PRINT. See PRINT.
    +Writes text to a file, formatted exactly as PRINT shows it: numbers are followed by a space, a comma moves to the next 14-column zone, a semicolon keeps the line going, and TAB(n), SPC(n), and PRINT #channel, USING work as they do for PRINT. See PRINT.
     
     .code
     PRINT #channel, expression [; | , expression ...]
    @@ -2358,7 +2358,7 @@ PRINT #channel, USING format$; expression [; expression ...]
     
     .h2 WIDTH #
     
    -Sets the line width of an open file, the screen or the printer: output longer than the width goes on in a new line, as in QBasic. 255 means no limit (the default).
    +Sets the line width of an open file, the screen, or the printer: output longer than the width goes on in a new line, as in QBasic. 255 means no limit (the default).
     
     .code
     WIDTH #channel, width
    @@ -2413,7 +2413,7 @@ GET #channel, [recordNum], variable
     PUT #channel, [recordNum], variable
     .endcode
     
    -In RANDOM mode recordNum is a 1-based record number and each record is LEN bytes (see OPEN). Numbers are stored in their binary form (Byte 1 byte, Integer and Boolean 2, Long 4, Single 4, Double, Currency and Date 8); a String is stored as a 2-byte length followed by its characters. PUT always writes a whole record, padding the unused part with zero bytes, and GET always advances to the next record; data longer than LEN raises error 59 before anything is written.
    +In RANDOM mode recordNum is a 1-based record number and each record is LEN bytes (see OPEN). Numbers are stored in their binary form (Byte 1 byte, Integer and Boolean 2, Long 4, Single 4, Double, Currency, and Date 8); a String is stored as a 2-byte length followed by its characters. PUT always writes a whole record, padding the unused part with zero bytes, and GET always advances to the next record; data longer than LEN raises error 59 before anything is written.
     
     In BINARY mode recordNum is a 1-based byte position. Numbers are stored as in RANDOM mode. PUT writes the characters of a String with no length prefix, and GET reads as many bytes as the String variable currently holds, so set its length first (for example with SPACE$ or by declaring it as STRING * n).
     
    @@ -2471,7 +2471,7 @@ pos = SEEK(channel)           ' Function: get current position
     
     DVX BASIC provides statements for working with the file system -- creating, deleting, renaming, and copying files and directories, and reading or setting file attributes.
     
    -A path may be written with / or \ between its parts, mixed too: OPEN "DATA/SCORES.TXT" and OPEN "DATA\SCORES.TXT" open the same file, and so do the FileOps, Shell and other library functions that take a path (SHELL commands are the exception: see SHELL). Paths DVX gives back -- CURDIR$, App.Path, App.Data, the file dialogs, FileOpsPathJoin$ and the other FileOps results -- use \, the separator DOS shows; FileOpsPathNative$ gives a path of your own that form before you show it.
    +A path may be written with / or \ between its parts, mixed too: OPEN "DATA/SCORES.TXT" and OPEN "DATA\SCORES.TXT" open the same file, and so do the FileOps, Shell, and other library functions that take a path (SHELL commands are the exception: see SHELL). Paths DVX gives back -- CURDIR$, App.Path, App.Data, the file dialogs, FileOpsPathJoin$, and the other FileOps results -- use \, the separator DOS shows; FileOpsPathNative$ gives a path of your own that form before you show it.
     
     .h2 KILL
     
    @@ -2567,7 +2567,7 @@ SetAttr "secret.dat", vbHidden + vbReadOnly
     
     .h2 CURDIR$ / DIR$
     
    -CURDIR$ returns the current directory. DIR$ returns the first file matching a pattern; subsequent calls with no arguments return additional matches (or "" when done). DIR$ returns only files: never ".", ".." or subdirectories. DIR$("C:\*.*") lists the root of C: (DIR$("C:*.*") the drive's current folder).
    +CURDIR$ returns the current directory. DIR$ returns the first file matching a pattern; subsequent calls with no arguments return additional matches (or "" when done). DIR$ returns only files: never ".", "..", or subdirectories. DIR$("C:\*.*") lists the root of C: (DIR$("C:*.*") the drive's current folder).
     
     .code
     cwd$ = CURDIR$
    @@ -2652,13 +2652,13 @@ bytes = FILELEN(filename$)
       VAL(s$)                                                      Double         Converts string s$ to a numeric value; stops at first non-numeric character
     .endtable
     
    -A negative count or position passed to LEFT$, RIGHT$, MID$, SPACE$ or STRING$, or a count above 65535 passed to SPACE$ or STRING$, raises error 5 (Illegal function call). INSTR follows OPTION COMPARE TEXT (a case-insensitive search), and an INSTR start position below 1 raises error 5.
    +A negative count or position passed to LEFT$, RIGHT$, MID$, SPACE$, or STRING$, or a count above 65535 passed to SPACE$ or STRING$, raises error 5 (Illegal function call). INSTR follows OPTION COMPARE TEXT (a case-insensitive search), and an INSTR start position below 1 raises error 5.
     
    -REPLACE$, JOIN$ and STRREVERSE$ may also be written without the $. Items in square brackets may be left out.
    +REPLACE$, JOIN$, and STRREVERSE$ may also be written without the $. Items in square brackets may be left out.
     
     .h2 Comparing Text
     
    -The compare argument of FILTER, INSTRREV, REPLACE$, SPLIT and STRCOMP chooses how text is matched: vbBinaryCompare (0) matches exactly, vbTextCompare (1) ignores upper and lower case, and vbUseCompareOption (-1, the default) follows OPTION COMPARE. Any other value raises error 5.
    +The compare argument of FILTER, INSTRREV, REPLACE$, SPLIT, and STRCOMP chooses how text is matched: vbBinaryCompare (0) matches exactly, vbTextCompare (1) ignores upper and lower case, and vbUseCompareOption (-1, the default) follows OPTION COMPARE. Any other value raises error 5.
     
     .code
     Print StrComp("apple", "Apple")                  ' 1 ("a" sorts after "A")
    @@ -2671,7 +2671,7 @@ Print InStrRev("C:\DVX\APPS\EDIT.APP", "\")     ' 12
     
     REPLACE$ returns the text from position start on, so REPLACE$("abcabc", "b", "x", 3) is "caxc" -- the characters before start are left out, as in Visual Basic. A count of -1 replaces every occurrence. An empty find$ returns the text unchanged. A start below 1 or a count below -1 raises error 5.
     
    -.h2 SPLIT, JOIN$ and FILTER
    +.h2 SPLIT, JOIN$, and FILTER
     
     SPLIT returns a string array numbered from 0. Assign it to a dynamic array (one declared with empty parentheses) and use UBOUND to find the last piece. The last piece keeps the rest of the text when limit stops the split. Splitting an empty string, or a limit of 0, gives an array with no elements: its UBOUND is -1. An empty delim$ gives the whole string as the only piece.
     
    @@ -2709,7 +2709,7 @@ Print Format$(5, "#,##0.00")          ' "    5.00"
     Print Format$(0.5, "PERCENT")         ' "50%"
     .endcode
     
    -The whole format string is one numeric field (for a number). The accepted format characters are # (digit or pad space), 0 (digit that is always printed; zero pads up to the first 0), . (decimal point), , (thousands separator), + and - (sign placement), $$ (floating dollar sign), ** (asterisk fill), ^^^^ (scientific notation) and the literal word PERCENT (multiplies by 100 and appends %). Unlike PRINT USING, a value wider than the field is printed in full without a % marker. See PRINT USING for details on each character.
    +The whole format string is one numeric field (for a number). The accepted format characters are # (digit or pad space), 0 (digit that is always printed; zero pads up to the first 0), . (decimal point), , (thousands separator), + and - (sign placement), $$ (floating dollar sign), ** (asterisk fill), ^^^^ (scientific notation), and the literal word PERCENT (multiplies by 100 and appends %). Unlike PRINT USING, a value wider than the field is printed in full without a % marker. See PRINT USING for details on each character.
     
     With no format string (or an empty one), FORMAT$ gives the value's plain text, as CSTR does: a number without the leading space STR$ puts before it (Format$(42) is "42"), a date in the usual m/d/yyyy h:mm:ss AM form, text as it is, True / False, and "" for Empty.
     
    @@ -2717,7 +2717,7 @@ FORMAT (without the $) is the same function.
     
     .h2 FORMAT$ with Dates
     
    -When the format string holds a date or time code, or is one of the named date formats, FORMAT$ formats its value as a date. The value may be a Date, a number (taken as a date's number of days) or a string that holds a date (anything else raises error 13). A Date with an empty format string gives the usual m/d/yyyy h:mm:ss AM form. The codes may be written in upper or lower case.
    +When the format string holds a date or time code, or is one of the named date formats, FORMAT$ formats its value as a date. The value may be a Date, a number (taken as a date's number of days), or a string that holds a date (anything else raises error 13). A Date with an empty format string gives the usual m/d/yyyy h:mm:ss AM form. The codes may be written in upper or lower case.
     
     .table
       Code          Result
    @@ -2828,7 +2828,7 @@ If length is omitted, MID$ replaces up to LEN(replacement$) characters; a length
     
     RND follows QBasic: RND or RND(n) with n > 0 returns the next number in the sequence, RND(0) repeats the last number, and RND(n) with n < 0 reseeds the generator from n (the same n always gives the same number). The result is a Double, 0 <= RND < 1.
     
    -INT, FIX and SGN return an Integer, widening to Long or Double when the value does not fit. EXP, ^ and the other functions raise error 6 (Overflow) when the result leaves the numeric range, and error 5 when it is undefined (for example a negative number to a fractional power).
    +INT, FIX, and SGN return an Integer, widening to Long or Double when the value does not fit. EXP, ^, and the other functions raise error 6 (Overflow) when the result leaves the numeric range, and error 5 when it is undefined (for example a negative number to a fractional power).
     
     .note info
     Use RANDOMIZE or RANDOMIZE TIMER to seed the generator so you don't get the same sequence each run.
    @@ -2836,7 +2836,7 @@ Use RANDOMIZE or RANDOMIZE TIMER to seed the generator so you don't get the same
     
     .h2 Color Functions
     
    -The color functions work with 24-bit colors packed into a Long the way Visual Basic packs them, &HBBGGRR: red is the low byte, green the middle byte and blue the high byte. RGB(255, 0, 0) and &HFF are red, RGB(0, 0, 255) and &HFF0000 are blue. ForeColor, BackColor, FillColor, the drawing methods of forms and picture boxes (PSet, Line, Circle, Point), the Canvas control and the color dialog all use this form (the console graphics statements of SCREEN modes use QBasic color numbers instead), as do the color constants vbBlack, vbRed, vbGreen, vbYellow, vbBlue, vbMagenta, vbCyan and vbWhite (see Predefined Constants).
    +The color functions work with 24-bit colors packed into a Long the way Visual Basic packs them, &HBBGGRR: red is the low byte, green the middle byte, and blue the high byte. RGB(255, 0, 0) and &HFF are red, RGB(0, 0, 255) and &HFF0000 are blue. ForeColor, BackColor, FillColor, the drawing methods of forms and picture boxes (PSet, Line, Circle, Point), the Canvas control, and the color dialog all use this form (the console graphics statements of SCREEN modes use QBasic color numbers instead), as do the color constants vbBlack, vbRed, vbGreen, vbYellow, vbBlue, vbMagenta, vbCyan, and vbWhite (see Predefined Constants).
     
     .table
       Function           Returns   Description
    @@ -2887,7 +2887,7 @@ Me.ForeColor = QBColor(14)     ' yellow
       CVAR(v)    Variant   Returns v unchanged as a Variant
     .endtable
     
    -Passing Null to any of these except CVAR raises error 94 (Invalid use of Null). Text passed to one that gives a number must hold a number (CINT("12") is 12); other text raises error 13 (Type mismatch). VARTYPE, TYPENAME and the IS functions (ISNULL, ISEMPTY, ISNUMERIC, ...) are described under Variants.
    +Passing Null to any of these except CVAR raises error 94 (Invalid use of Null). Text passed to one that gives a number must hold a number (CINT("12") is 12); other text raises error 13 (Type mismatch). VARTYPE, TYPENAME, and the IS functions (ISNULL, ISEMPTY, ISNUMERIC, ...) are described under Variants.
     
     .link lang.variants See also: Variants
     
    @@ -2917,7 +2917,7 @@ Passing Null to any of these except CVAR raises error 94 (Invalid use of Null).
     
     .h1 Date and Time Functions
     
    -These functions work with Date values (see Data Types). Wherever a date is expected, a Date, a number of days since 12/30/1899 or a string holding a date may be given; text that is not a date raises error 13 (Type mismatch).
    +These functions work with Date values (see Data Types). Wherever a date is expected, a Date, a number of days since 12/30/1899, or a string holding a date may be given; text that is not a date raises error 13 (Type mismatch).
     
     .table
       Function                                                 Returns   Description
    @@ -2944,11 +2944,11 @@ These functions work with Date values (see Data Types). Wherever a date is expec
       WEEKDAYNAME(n [, abbreviate [, firstDay]])               String    Name of day n of the week counted from firstDay (default vbSunday)
     .endtable
     
    -NOW, DATE and TIME need no parentheses. DATE$ and TIME$ still return the date and time as text.
    +NOW, DATE, and TIME need no parentheses. DATE$ and TIME$ still return the date and time as text.
     
     .h2 Intervals
     
    -DATEADD, DATEDIFF and DATEPART take the interval as a string, in upper or lower case:
    +DATEADD, DATEDIFF, and DATEPART take the interval as a string, in upper or lower case:
     
     .table
       Interval   Meaning
    @@ -2969,7 +2969,7 @@ Any other interval raises error 5 (Illegal function call).
     
     .h2 DATESERIAL and TIMESERIAL
     
    -A year from 0 to 29 means 2000 to 2029 and one from 30 to 99 means 1930 to 1999. Months, days, hours, minutes and seconds outside their usual range carry into the next or previous unit, so DATESERIAL(2000, 13, 1) is 1/1/2001, DATESERIAL(2000, 3, 0) is the last day of February 2000 and TIMESERIAL(0, 90, 0) is 1:30 AM. Fractional arguments are rounded. A result before 1/1/100 or after 12/31/9999 raises error 5.
    +A year from 0 to 29 means 2000 to 2029 and one from 30 to 99 means 1930 to 1999. Months, days, hours, minutes, and seconds outside their usual range carry into the next or previous unit, so DATESERIAL(2000, 13, 1) is 1/1/2001, DATESERIAL(2000, 3, 0) is the last day of February 2000, and TIMESERIAL(0, 90, 0) is 1:30 AM. Fractional arguments are rounded. A result before 1/1/100 or after 12/31/9999 raises error 5.
     
     .code
     Dim lastDay As Date
    @@ -2979,7 +2979,7 @@ Print Day(lastDay)
     
     .h2 DATEADD
     
    -Adding months, quarters or years keeps the day of the month where it can; when the new month is shorter the day becomes its last day, so DATEADD("m", 1, #1/31/2000#) is 2/29/2000. The time of day is kept. "y", "d" and "w" all add days and "ww" adds weeks. A fractional n drops its fraction. A result outside the Date range raises error 5.
    +Adding months, quarters, or years keeps the day of the month where it can; when the new month is shorter the day becomes its last day, so DATEADD("m", 1, #1/31/2000#) is 2/29/2000. The time of day is kept. "y", "d", and "w" all add days and "ww" adds weeks. A fractional n drops its fraction. A result outside the Date range raises error 5.
     
     .code
     Print DateAdd("m", 1, #1/31/2001#)      ' 2/28/2001
    @@ -3016,7 +3016,7 @@ Print DatePart("q", #5/15/2000#)                               ' 2
     Print DatePart("ww", #1/1/2000#, vbSunday, vbFirstFourDays)    ' 52
     .endcode
     
    -.h2 ISDATE, MONTHNAME and WEEKDAYNAME
    +.h2 ISDATE, MONTHNAME, and WEEKDAYNAME
     
     ISDATE is True for a Date and for a string that holds a valid date or time; numbers are not dates to ISDATE. MONTHNAME with a month outside 1 to 12, and WEEKDAYNAME with n outside 1 to 7, raise error 5.
     
    @@ -3061,7 +3061,7 @@ Print MonthName(Month(Date)); " "; WeekdayName(Weekday(Date), True)
     
     The dim argument of LBOUND and UBOUND must be a constant from 1 to 8.
     
    -File sizes and positions are Longs, so they stop at 2 GB (2,147,483,647 bytes). A disk can hold bigger files (up to 4 GB on FAT32); LOF, FILELEN, LOC and SEEK raise error 6 (Overflow) for a size or position past 2 GB rather than give a wrong number, and GET and PUT do the same for a record that would end past it. EOF stays right while the position is below 2 GB. FileOpsListSize and FileOpsTreeBytes (fileops.bas) give any file's size as a Double.
    +File sizes and positions are Longs, so they stop at 2 GB (2,147,483,647 bytes). A disk can hold bigger files (up to 4 GB on FAT32); LOF, FILELEN, LOC, and SEEK raise error 6 (Overflow) for a size or position past 2 GB rather than give a wrong number, and GET and PUT do the same for a record that would end past it. EOF stays right while the position is below 2 GB. FileOpsListSize and FileOpsTreeBytes (fileops.bas) give any file's size as a Double.
     
     .topic lang.func.misc
     .title Miscellaneous Functions
    @@ -3090,7 +3090,7 @@ File sizes and positions are Longs, so they stop at 2 GB (2,147,483,647 bytes).
       SHELL(cmd$)            Integer   Exit code of the command (also usable as a statement, discarding the code)
       IIF(cond, a, b)        (any)     a when cond is True, b when it is False
       CHOOSE(n, a, b, ...)   (any)     The nth value of the list (1 = a), or Null when there is no nth value; up to 31 values
    -  LOADPICTURE([file$ [, size [, colordepth [, x, y]]]])   Object    A Picture object for a picture property (DragIcon, Picture), with Width, Height and Type; no file (or "") gives an empty picture
    +  LOADPICTURE([file$ [, size [, colordepth [, x, y]]]])   Object    A Picture object for a picture property (DragIcon, Picture), with Width, Height, and Type; no file (or "") gives an empty picture
     .endtable
     
     DATE$ and TIME$ return text; the Date and Time Functions return Date values.
    @@ -3106,9 +3106,9 @@ Print Choose(Weekday(Date), "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")
     
     .h2 LOADPICTURE
     
    -LOADPICTURE(file$) reads a picture file (BMP, PNG, JPEG or GIF) -- or the icon of a DVX program, widget or library file (.app, .wgt, .lib) -- and returns a Picture object (TypeName "Picture") that can be given to a picture property: a control's DragIcon, the Picture of an Image or ImageButton. Use SET, or a plain assignment, as with a file name; file names still work too. LOADPICTURE() or LOADPICTURE("") gives an empty picture, which clears the property. A missing file raises error 53 (File not found), a file that is not a picture error 481 (Invalid picture), so the check happens where the picture is loaded. Loading the same file again gives the same Picture object. Reading a picture property back gives a Picture object too (TypeName "Picture"), as in Visual Basic; used as a value (printed, joined to text, compared) it stands for its file name, so code that treats the property as a file name keeps working. The name of a program, widget or library file works wherever a picture file's name does (a Picture property, DragIcon, PaintPicture, a Toolbar's AddButton) and shows its icon; such a file without an icon is not a picture (error 481).
    +LOADPICTURE(file$) reads a picture file (BMP, PNG, JPEG, or GIF) -- or the icon of a DVX program, widget, or library file (.app, .wgt, .lib) -- and returns a Picture object (TypeName "Picture") that can be given to a picture property: a control's DragIcon, the Picture of an Image or ImageButton. Use SET, or a plain assignment, as with a file name; file names still work too. LOADPICTURE() or LOADPICTURE("") gives an empty picture, which clears the property. A missing file raises error 53 (File not found), a file that is not a picture error 481 (Invalid picture), so the check happens where the picture is loaded. Loading the same file again gives the same Picture object. Reading a picture property back gives a Picture object too (TypeName "Picture"), as in Visual Basic; used as a value (printed, joined to text, compared) it stands for its file name, so code that treats the property as a file name keeps working. The name of a program, widget, or library file works wherever a picture file's name does (a Picture property, DragIcon, PaintPicture, a Toolbar's AddButton) and shows its icon; such a file without an icon is not a picture (error 481).
     
    -The optional size and colordepth arguments are Visual Basic's: size is vbLPSmall (0), vbLPLarge (1), vbLPSmallShell (2), vbLPLargeShell (3) or vbLPCustom (4, with x and y giving the size), colordepth vbLPDefault (0), vbLPMonochrome (1), vbLPVGAColor (2) or vbLPColor (3). They choose among the images of an icon file; every DVX picture is a single bitmap, so, as for a bitmap in Visual Basic, they are checked and otherwise make no difference. A value outside these, or a negative x or y, raises error 5.
    +The optional size and colordepth arguments are Visual Basic's: size is vbLPSmall (0), vbLPLarge (1), vbLPSmallShell (2), vbLPLargeShell (3), or vbLPCustom (4, with x and y giving the size), colordepth vbLPDefault (0), vbLPMonochrome (1), vbLPVGAColor (2), or vbLPColor (3). They choose among the images of an icon file; every DVX picture is a single bitmap, so, as for a bitmap in Visual Basic, they are checked and otherwise make no difference. A value outside these, or a negative x or y, raises error 5.
     
     A Picture object has three properties, which can only be read:
     
    @@ -3314,7 +3314,7 @@ Creates a control on the given form. typeName$ is the control type (e.g. "Comman
     SETEVENT ctrlRef, eventName$, handlerName$
     .endcode
     
    -Wires an arbitrary SUB as the handler for an event of a control, a form (Me or a form reference) or a menu item. The default event-handler convention (ControlName_EventName) is used automatically for controls defined in a .frm file; SetEvent is for controls you create dynamically or for reusing a single handler across multiple controls. A handler declared without parameters is called without the event's arguments.
    +Wires an arbitrary SUB as the handler for an event of a control, a form (Me or a form reference), or a menu item. The default event-handler convention (ControlName_EventName) is used automatically for controls defined in a .frm file; SetEvent is for controls you create dynamically or for reusing a single handler across multiple controls. A handler declared without parameters is called without the event's arguments.
     
     .h3 RemoveControl
     
    @@ -3330,7 +3330,7 @@ Removes a control from a form. A later reference to the removed control raises a
     SET formVar = LOADFORMTEXT(frmText$)
     .endcode
     
    -Loads a form from the text of a .frm file -- one a program made, read from a file or got from somewhere else -- and returns it, loaded but hidden like Load (its Load event runs; call Show). A form whose name is already loaded becomes another copy of its own, as with New. The form's controls fire events to procedures named after them in the running program (Gen1_Load, Go_Click), as for any form; a code section after the form's End is not compiled. Text that is not a form raises error 5 (Illegal function call). Form designers use it to show a .frm file being edited; Controls, CallByName and the designer.bas functions then read and change it.
    +Loads a form from the text of a .frm file -- one a program made, read from a file, or got from somewhere else -- and returns it, loaded but hidden like Load (its Load event runs; call Show). A form whose name is already loaded becomes another copy of its own, as with New. The form's controls fire events to procedures named after them in the running program (Gen1_Load, Go_Click), as for any form; a code section after the form's End is not compiled. Text that is not a form raises error 5 (Illegal function call). Form designers use it to show a .frm file being edited; Controls, CallByName, and the designer.bas functions then read and change it.
     
     .code
     Dim f As Object
    @@ -3552,7 +3552,7 @@ Attribute VB_Name = "Account"
     Option Explicit
     .endcode
     
    -The header is everything before the first line of code: blank lines, a VERSION line, a BEGIN ... END block and Attribute VB_... lines, in that order. All of it is optional, and a Visual Basic .cls file can be added as it is. The class's name comes from the Attribute VB_Name line; without one, the name is the file's name without .cls (ACCOUNT.CLS makes a class named ACCOUNT). The class name is the type name used in DIM ... AS, NEW, TypeOf and TYPENAME.
    +The header is everything before the first line of code: blank lines, a VERSION line, a BEGIN ... END block and Attribute VB_... lines, in that order. All of it is optional, and a Visual Basic .cls file can be added as it is. The class's name comes from the Attribute VB_Name line; without one, the name is the file's name without .cls (ACCOUNT.CLS makes a class named ACCOUNT). The class name is the type name used in DIM ... AS, NEW, TypeOf, and TYPENAME.
     
     .h2 What a Class Contains
     
    @@ -3570,7 +3570,7 @@ Outside its procedures, a class module may hold only declarations: member variab
       Friend procedure                   Friend Sub / Function / Property ...       obj.name, by the program's own code
     .endtable
     
    -A Sub, Function or Property is Public unless it is marked Private; Private procedures can be called only by the class's own code. A Friend one, as in Visual Basic, can be called by the rest of the program through a variable declared as the class, but it is not part of the object's late-bound interface: through As Object or As Variant it raises error 438, and an Implements interface does not include it. Inside the class's procedures the member variables are used by name, with no SHARED needed, and each call works with the variables of the object it was called for. The class's code can also use the program's DIM SHARED (or PUBLIC) module variables, procedures and constants.
    +A Sub, Function, or Property is Public unless it is marked Private; Private procedures can be called only by the class's own code. A Friend one, as in Visual Basic, can be called by the rest of the program through a variable declared as the class, but it is not part of the object's late-bound interface: through As Object or As Variant it raises error 438, and an Implements interface does not include it. Inside the class's procedures the member variables are used by name, with no SHARED needed, and each call works with the variables of the object it was called for. The class's code can also use the program's DIM SHARED (or PUBLIC) module variables, procedures, and constants.
     
     .h2 Properties
     
    @@ -3645,15 +3645,15 @@ End Sub
     
     .h2 Using Objects
     
    -When the compiler knows an object's class -- a variable, array, parameter, member variable or FUNCTION declared As the class name or As Collection, New ClassName, or Me in a class module -- its members are found when the program is compiled (early binding), as in Visual Basic. A misspelled member, a Private one, a Sub used where a value is needed, a property used as a statement, assigning a property that has no Property Let or Set, reading one that has no Property Get, and the wrong number of arguments are then compile errors that point at the member. Calls made this way are also faster. A class's own members can be used anywhere in the class, also above the line that declares them, and through Me a class reaches its Private members as well (Me.mTotal).
    +When the compiler knows an object's class -- a variable, array, parameter, member variable, or FUNCTION declared As the class name or As Collection, New ClassName, or Me in a class module -- its members are found when the program is compiled (early binding), as in Visual Basic. A misspelled member, a Private one, a Sub used where a value is needed, a property used as a statement, assigning a property that has no Property Let or Set, reading one that has no Property Get, and the wrong number of arguments are then compile errors that point at the member. Calls made this way are also faster. A class's own members can be used anywhere in the class, also above the line that declares them, and through Me a class reaches its Private members as well (Me.mTotal).
     
     Through a variable declared As Object or As Variant the member is found by name when the program runs (late binding): obj.Deposit 5 looks for a Public member named Deposit in whatever object obj holds at that moment. A misspelled or Private member then raises error 438 when the line runs, and the wrong number of arguments raises error 450. Using a member of Nothing raises error 91 either way. An object of another class that reaches a variable of a class anyway (for example through a ByRef As Object argument) has its members found by name, as late binding would.
     
    -An argument that can never fit an object parameter of a SUB, FUNCTION or method is a compile error too: a number or text, or an object of another class than the parameter's (unless that class Implements it).
    +An argument that can never fit an object parameter of a SUB, FUNCTION, or method is a compile error too: a number or text, or an object of another class than the parameter's (unless that class Implements it).
     
     An object can be kept in a variable declared as its class name, As Object, or As Variant, in an array, in a TYPE field, in a Collection, or in another object's member variable. It can be passed to a SUB or FUNCTION and returned from a FUNCTION (assign the result with SET inside the function, Set MakeAccount = acct). TYPENAME gives an object's class name, and TypeOf obj Is ClassName tests it; Is tells whether two variables refer to the same object.
     
    -Arguments of a class's procedures follow the rules of SUB and FUNCTION calls when the object variable is declared as the class name: a parameter without BYVAL gets the caller's variable, so the procedure can change it, an omitted OPTIONAL argument gets its default, and a ParamArray collects the rest. When the object is reached through a variable declared As Object or As Variant, a variable or array element given as an argument is passed by reference too if the procedure's parameter is not BYVAL (Visual Basic's rule; other expressions go by value). What the procedure stores in such a parameter is converted to the type of the caller's variable (an Integer stays an Integer, as in Visual Basic). A TYPE field given as an argument (obj.Move rec.x, obj.Move pts(1).x) is passed by reference as well (a BYVAL parameter gets its value). The methods of forms and controls always take their arguments by value. An argument left out in the middle of a call (obj.Report a, , c) or at the end is an OPTIONAL parameter's default value, else its empty value: 0, "" and so on for a typed parameter, Missing for a Variant (ISMISSING is True). Leaving out a parameter that is not OPTIONAL raises error 449 (Argument not optional).
    +Arguments of a class's procedures follow the rules of SUB and FUNCTION calls when the object variable is declared as the class name: a parameter without BYVAL gets the caller's variable, so the procedure can change it, an omitted OPTIONAL argument gets its default, and a ParamArray collects the rest. When the object is reached through a variable declared As Object or As Variant, a variable or array element given as an argument is passed by reference too if the procedure's parameter is not BYVAL (Visual Basic's rule; other expressions go by value). What the procedure stores in such a parameter is converted to the type of the caller's variable (an Integer stays an Integer, as in Visual Basic). A TYPE field given as an argument (obj.Move rec.x, obj.Move pts(1).x) is passed by reference as well (a BYVAL parameter gets its value). The methods of forms and controls always take their arguments by value. An argument left out in the middle of a call (obj.Report a, , c) or at the end is an OPTIONAL parameter's default value, else its empty value: 0, "", and so on for a typed parameter, Missing for a Variant (ISMISSING is True). Leaving out a parameter that is not OPTIONAL raises error 449 (Argument not optional).
     
     An object parameter is itself passed like any other parameter: without BYVAL, SET on the parameter changes the caller's variable (a SUB can make a new object for the caller); with BYVAL, the procedure gets its own reference to the same object, and SET on it does not affect the caller.
     
    @@ -3664,7 +3664,7 @@ result = CALLBYNAME(object, memberName$, callType [, arguments...])
     CALLBYNAME object, memberName$, callType [, arguments...]
     .endcode
     
    -Uses the member of an object whose name the program has in a string, as if it had been written after a dot: a form, a control, a class instance or a Collection. callType says what to do: vbGet reads a property (with arguments, a property that takes them), vbLet assigns it the last argument, vbSet assigns it an object (the last argument), vbMethod calls a method or function with the arguments. The function gives the property's value or the method's result (Empty for vbLet and vbSet); as a statement the result is dropped. Another callType raises error 5 (Illegal function call), as does vbLet or vbSet without a value; a member the object does not have raises the error the same member written after a dot would. A form designer's property window uses it to read and set the properties designer.bas lists for a control type.
    +Uses the member of an object whose name the program has in a string, as if it had been written after a dot: a form, a control, a class instance, or a Collection. callType says what to do: vbGet reads a property (with arguments, a property that takes them), vbLet assigns it the last argument, vbSet assigns it an object (the last argument), vbMethod calls a method or function with the arguments. The function gives the property's value or the method's result (Empty for vbLet and vbSet); as a statement the result is dropped. Another callType raises error 5 (Illegal function call), as does vbLet or vbSet without a value; a member the object does not have raises the error the same member written after a dot would. A form designer's property window uses it to read and set the properties designer.bas lists for a control type.
     
     .code
     Dim prop As String
    @@ -3685,7 +3685,7 @@ Attribute Item.VB_UserMemId = 0
     
     With Item as the default member, obj(3) means obj.Item(3), and obj(3) = x calls Property Let Item. Collection's default member is Item, which is why c(1) and c("key") work. A call's result can be indexed directly, too: c(1)(0) is element 0 of the array item 1 holds, and Split(s, ",")(2) the third part.
     
    -Where a value is needed -- in arithmetic, a comparison, text, PRINT, a typed variable or a typed parameter -- an object stands for its default member's value, as in Visual Basic: with Value as the default member, total = obj + 1 reads obj.Value, and obj = 9 (without SET) stores 9 through it. A member variable can be the default member too (Attribute Label.VB_UserMemId = 0). An object without a default member used as a value raises error 13. The object name on its own still means the object itself wherever an object is expected: SET, Is, passing it to an object parameter, adding it to a Collection. Controls have default properties as well (Text1 means Text1.Text); see the control overview.
    +Where a value is needed -- in arithmetic, a comparison, text, PRINT, a typed variable, or a typed parameter -- an object stands for its default member's value, as in Visual Basic: with Value as the default member, total = obj + 1 reads obj.Value, and obj = 9 (without SET) stores 9 through it. A member variable can be the default member too (Attribute Label.VB_UserMemId = 0). An object without a default member used as a value raises error 13. The object name on its own still means the object itself wherever an object is expected: SET, Is, passing it to an object parameter, adding it to a Collection. Controls have default properties as well (Text1 means Text1.Text); see the control overview.
     
     .h2 WITH...END WITH
     
    @@ -3729,13 +3729,13 @@ NoMoney:
     .h2 Rules and Limits
     
     .list
    -.item A member's name cannot be a reserved word. Names such as Name, Show, Hide, Load, Print, Open, Close, Line, Input, Get, Put, Type, Error and Timer are keywords in DVX BASIC and cannot be used; Text, Caption, Value, Count, Item and Add are fine.
    +.item A member's name cannot be a reserved word. Names such as Name, Show, Hide, Load, Print, Open, Close, Line, Input, Get, Put, Type, Error, and Timer are keywords in DVX BASIC and cannot be used; Text, Caption, Value, Count, Item, and Add are fine.
     .item SHARED and DIM SHARED cannot be used in a class module; a class's code reaches the program's module variables that are declared DIM SHARED or PUBLIC.
     .item A STATIC variable in a class procedure is kept per object, as in Visual Basic: each object has its own copy.
     .item A class has at most 65536 member variables, its procedures' STATIC variables included; more fails to compile with "Too many member variables in ClassName".
     .item Class_Initialize and Class_Terminate take no parameters.
     .item A class module cannot hold statements outside its procedures; the compiler reports "Only declarations are allowed outside procedures in a class module".
    -.item NEW, PROPERTY, PUBLIC, PRIVATE and TYPEOF are reserved words. VARIANT, OBJECT, COLLECTION, EMPTY, NULL, EACH and IN are not.
    +.item NEW, PROPERTY, PUBLIC, PRIVATE, and TYPEOF are reserved words. VARIANT, OBJECT, COLLECTION, EMPTY, NULL, EACH, and IN are not.
     .endlist
     
     .link lang.events See also: Events
    @@ -3779,11 +3779,11 @@ Public Sub Start()
     End Sub
     .endcode
     
    -An event may have parameters but no return value. Its parameters cannot be OPTIONAL or a PARAMARRAY, and an event is always public (Public Event and Event mean the same; Private Event is an error). RAISEEVENT can only be used inside the class's own procedures, with exactly the arguments the event declares. A parameter without BYVAL is passed by reference, so a handler can change the variable given for it -- the cancel flag above is how a handler asks the class to stop. This holds for a variable, an array element (a(i)) and a TYPE field (rec.f, arr(i).f), as in Visual Basic. When several handlers get the event they all get the same variable, so each sees what the one before it stored; every store is converted to the variable's or field's type at once, as an assignment would be. While the handlers run, the arrays whose elements (or elements' fields) were given are locked: REDIM of one of them (or ERASE of a dynamic one) raises error 10. An expression, or a variable in its own parentheses, given for a by-reference parameter is passed as one temporary value that every handler shares, as in Visual Basic: the variable itself does not change, but a handler sees what the one before it stored. An argument is converted to the type of a typed event parameter, with the rules of SUB calls (a variable of another type given by reference is a compile error).
    +An event may have parameters but no return value. Its parameters cannot be OPTIONAL or a PARAMARRAY, and an event is always public (Public Event and Event mean the same; Private Event is an error). RAISEEVENT can only be used inside the class's own procedures, with exactly the arguments the event declares. A parameter without BYVAL is passed by reference, so a handler can change the variable given for it -- the cancel flag above is how a handler asks the class to stop. This holds for a variable, an array element (a(i)), and a TYPE field (rec.f, arr(i).f), as in Visual Basic. When several handlers get the event they all get the same variable, so each sees what the one before it stored; every store is converted to the variable's or field's type at once, as an assignment would be. While the handlers run, the arrays whose elements (or elements' fields) were given are locked: REDIM of one of them (or ERASE of a dynamic one) raises error 10. An expression, or a variable in its own parentheses, given for a by-reference parameter is passed as one temporary value that every handler shares, as in Visual Basic: the variable itself does not change, but a handler sees what the one before it stored. An argument is converted to the type of a typed event parameter, with the rules of SUB calls (a variable of another type given by reference is a compile error).
     
     .h2 Handling Events
     
    -In a module, a form or another class module, declare a variable of the class WithEvents, outside any procedure, and write a SUB named variable_EventName with the same parameters as the event:
    +In a module, a form, or another class module, declare a variable of the class WithEvents, outside any procedure, and write a SUB named variable_EventName with the same parameters as the event:
     
     .code
     Dim WithEvents dl As Downloader
    @@ -3838,10 +3838,10 @@ Lifetime rule: listening never keeps an object alive. The object that raises an
     .h2 Rules
     
     .list
    -.item WITHEVENTS goes with DIM, PUBLIC or PRIVATE at the top level of a module, form or class module (not inside a procedure), for a single variable declared as a class module's name. It cannot be an array and cannot use New: Set it to an object in code.
    +.item WITHEVENTS goes with DIM, PUBLIC, or PRIVATE at the top level of a module, form, or class module (not inside a procedure), for a single variable declared as a class module's name. It cannot be an array and cannot use New: Set it to an object in code.
     .item A WithEvents variable is visible to the procedures of its module or form without SHARED, as the handlers and the code that sets it need; in a class module it is a member variable of each object.
    -.item Handlers are SUBs named variable_EventName in the same module, form or class module as the variable.
    -.item EVENT, RAISEEVENT, WITHEVENTS and IMPLEMENTS are not reserved words: they only have their meaning where a name follows them in those places, so a program may still use them as its own names.
    +.item Handlers are SUBs named variable_EventName in the same module, form, or class module as the variable.
    +.item EVENT, RAISEEVENT, WITHEVENTS, and IMPLEMENTS are not reserved words: they only have their meaning where a name follows them in those places, so a program may still use them as its own names.
     .endlist
     
     .link lang.classes See also: Objects and Class Modules
    @@ -3891,7 +3891,7 @@ Print TypeOf s Is IShape    ' True
     Print TypeName(s)           ' Circle
     .endcode
     
    -Through a variable declared as the interface, a member always means the interface's member, even when the class has a public member of the same name of its own. Through the class's own type, As Object or As Variant, the class's own public members are used. SET into a variable of the interface type accepts an object of any class that Implements it (and of the interface class itself); TypeOf obj Is Interface is True for them. A class can implement several interfaces with one IMPLEMENTS line for each. An argument left out of a call through the interface takes the default the interface's procedure declares, as in Visual Basic, even if the implementing procedure declares another. Friend procedures are not part of an interface.
    +Through a variable declared as the interface, a member always means the interface's member, even when the class has a public member of the same name of its own. Through the class's own type, As Object, or As Variant, the class's own public members are used. SET into a variable of the interface type accepts an object of any class that Implements it (and of the interface class itself); TypeOf obj Is Interface is True for them. A class can implement several interfaces with one IMPLEMENTS line for each. An argument left out of a call through the interface takes the default the interface's procedure declares, as in Visual Basic, even if the implementing procedure declares another. Friend procedures are not part of an interface.
     
     .link lang.classes See also: Objects and Class Modules
     .link lang.events See also: Events
    @@ -3909,7 +3909,7 @@ Through a variable declared as the interface, a member always means the interfac
     
     .h1 Collection Object
     
    -A Collection is a ready-made object that holds a list of items of any kind -- numbers, text, objects -- in order. Items can be looked up by position or by a text key, added anywhere in the list and removed, and FOR EACH walks through them. The list grows as needed; there is no size to declare.
    +A Collection is a ready-made object that holds a list of items of any kind -- numbers, text, objects -- in order. Items can be looked up by position or by a text key, added anywhere in the list, and removed, and FOR EACH walks through them. The list grows as needed; there is no size to declare.
     
     .code
     Dim fruit As New Collection
    @@ -3952,10 +3952,10 @@ Through a variable declared As Collection the members are checked when the progr
     .table
       Error   When
       -----   ----
    -  5       A key that is not in the Collection (Item, Remove, before or after), or Add given both before and after
    +  5       A key that is not in the Collection (Item, Remove, before, or after), or Add given both before and after
       9       A position that is not in the Collection
       13      A key that is not text
    -  438     A member other than Add, Remove, Item and Count, or assigning one, through As Object or As Variant
    +  438     A member other than Add, Remove, Item, and Count, or assigning one, through As Object or As Variant
       450     The wrong number of arguments, through As Object or As Variant
       457     Add with a key that is already in use
     .endtable
    @@ -4131,7 +4131,7 @@ Returns the last error message for the database.
     msg$ = SQLError$(db)
     .endcode
     
    -When a database file could not be opened, read or written, the message ends with the reason in parentheses, for example "unable to open database file (No such file or directory)". If another program, or another handle in your own program, is changing the same database at that moment, the message is "database is locked": try again once the other change is finished, and free result sets with SQLFreeResult as soon as you are done with them, because an open result set stops other handles from saving changes.
    +When a database file could not be opened, read, or written, the message ends with the reason in parentheses, for example "unable to open database file (No such file or directory)". If another program, or another handle in your own program, is changing the same database at that moment, the message is "database is locked": try again once the other change is finished, and free result sets with SQLFreeResult as soon as you are done with them, because an open result set stops other handles from saving changes.
     
     .h2 Complete SQL Example
     
    @@ -4239,13 +4239,13 @@ The Screen object is the DVX desktop, as in Visual Basic: its size, so a program
       --------------------  -------   ---   -----------
       Screen.Width          Long      R     Width of the desktop in pixels
       Screen.Height         Long      R     Height of the desktop in pixels
    -  Screen.BitsPerPixel   Long      R     Colour depth of the display: 8, 15, 16, 24 or 32 bits per pixel
    +  Screen.BitsPerPixel   Long      R     Colour depth of the display: 8, 15, 16, 24, or 32 bits per pixel
       Screen.MousePointer   Integer   R/W   vbHourglass (11) for the busy pointer while a long job runs; another pointer (vbArrow, vbCrosshair, vbSizeNS, vbSizeWE, vbSizeNWSE, vbSizeNESW, vbNoDrop: see Mouse Constants) over every form of the program, whatever their controls' MousePointer; vbDefault (0) back to each control's own
       Screen.ActiveControl  Control   R     The control of the program that has the keyboard focus, Nothing when none has it (another program's window has the focus)
       Screen.DebugLayout    Boolean   R/W   True draws a coloured border around every layout container (VBox, HBox, Frame ...) in every window, to see how a form is laid out; False removes them
     .endtable
     
    -The sizes are in pixels, the unit of a form's Left, Top, Width and Height in DVX (Visual Basic uses twips there).
    +The sizes are in pixels, the unit of a form's Left, Top, Width, and Height in DVX (Visual Basic uses twips there).
     
     .code
     ' Put the form at the right edge, 40 pixels from the top
    @@ -4282,7 +4282,7 @@ Sub mnuCascade_Click ()
     End Sub
     .endcode
     
    -Any other property, method or value stops the program with an error. The QBasic statement SCREEN and function SCREEN(row, column) are unaffected: Screen followed by a dot is the object. A control or form of your own named Screen takes the name's place.
    +Any other property, method, or value stops the program with an error. The QBasic statement SCREEN and function SCREEN(row, column) are unaffected: Screen followed by a dot is the object. A control or form of your own named Screen takes the name's place.
     
     .topic lang.ini
     .title INI Functions
    @@ -4475,7 +4475,7 @@ Add an icon flag to the button style (with + or OR) to display an icon in the me
       vbModal    1       Show form as modal dialog
     .endtable
     
    -.h2 Window State, Arrangement and Mouse Pointer
    +.h2 Window State, Arrangement, and Mouse Pointer
     
     A form's WindowState and Screen.Arrange (see Form and Screen Object); the MousePointer values are under Mouse Constants.
     
    @@ -4508,7 +4508,7 @@ Bit values used with GETATTR and SETATTR.
     
     .h2 Compare Constants
     
    -The compare argument of FILTER, INSTRREV, REPLACE$, SPLIT and STRCOMP.
    +The compare argument of FILTER, INSTRREV, REPLACE$, SPLIT, and STRCOMP.
     
     .table
       Constant             Value   Description
    @@ -4541,7 +4541,7 @@ The firstDay and firstWeek arguments of the date functions, and WEEKDAY results.
     
     .h2 Drag and Drop Constants
     
    -The DragMode property, the Drag method and the State argument of DragOver.
    +The DragMode property, the Drag method, and the State argument of DragOver.
     
     .table
       Constant      Value   Description
    @@ -4565,7 +4565,7 @@ The DragMode property, the Drag method and the State argument of DragOver.
     
     .h2 Mouse Constants
     
    -The Button argument of the MouseDown, MouseUp and MouseMove events (several bits when several buttons are down), and the values of MousePointer (controls, forms and Screen).
    +The Button argument of the MouseDown, MouseUp, and MouseMove events (several bits when several buttons are down), and the values of MousePointer (controls, forms, and Screen).
     
     .table
       Constant         Value   Description
    @@ -4735,6 +4735,12 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic
     .index DlgColor
     .index DlgButtons
     .index Color Dialog
    +.index DlgFont
    +.index DlgFontName$
    +.index DlgFontSize
    +.index DlgFontStyle
    +.index DLG_FONT_FIXED
    +.index Font Dialog
     .index sql.bas
     .index comm.bas
     .index resource.bas
    @@ -4878,10 +4884,21 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic
     .index Long File Names
     .index system.bas
     .index SysSetVideoMode
    +.index font.bas
    +.index richtext.bas
    +.index FontFamilyCount
    +.index FontFamilyName$
    +.index FontTextWidth
    +.index Fonts
    +.index RichEdit
    +.index RichLabel
    +.index Font properties
     .index SysSetColor
     .index SysGetColor
     .index SysLoadTheme
     .index SysSetWallpaper
    +.index SysSetTextSmoothing
    +.index Text smoothing
     .index SysSetMouse
     .index PrefsDvxIni$
     .index PrefsChoiceLabel$
    @@ -5035,30 +5052,32 @@ DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory
     .table
       File           Provides
       ----           --------
    -  commdlg.bas    File Open/Save dialogs (DlgFileOpenIn$ and DlgFileSaveIn$ start in a folder you name), input dialogs, choice dialogs, save-prompt dialogs, the color dialog, message boxes with your own buttons (DlgButtons)
    +  commdlg.bas    File Open/Save dialogs (DlgFileOpenIn$ and DlgFileSaveIn$ start in a folder you name), input dialogs, choice dialogs, save-prompt dialogs, the color dialog, the font dialog (DlgFont), message boxes with your own buttons (DlgButtons)
       sql.bas        SQLite database access (SQLOpen, SQLQuery, SQLNext, SQLField$, etc.)
    -  comm.bas       Serial-port I/O (raw and packet) and optional encrypted links; port settings read back and changed, why a call failed, breaks, ring and line errors (SerOpen, SerSet, SerLastError, CommOpen, CommHandshakeStep, ...)
    -  conn.bas       Connections the way Terminal makes them: a serial port, a modem that dials, the secure link to the DVX host proxy or Telnet, shown on a Terminal control, with capture, answerback and file transfers (ConnCreate, ConnOpen, ConnPoll, ConnAttach, ConnSendFiles, ...)
    -  xfer.bas       File transfers with XMODEM, XMODEM-1K, YMODEM and ZMODEM, on a serial port or a connection, with their progress and questions (XferSendFile, XferStatus, XferBytes, XferAnswer, ...)
    +  comm.bas       Serial-port I/O (raw and packet) and optional encrypted links; port settings read back and changed, why a call failed, breaks, ring, and line errors (SerOpen, SerSet, SerLastError, CommOpen, CommHandshakeStep, ...)
    +  conn.bas       Connections the way Terminal makes them: a serial port, a modem that dials, the secure link to the DVX host proxy, or Telnet, shown on a Terminal control, with capture, answerback, and file transfers (ConnCreate, ConnOpen, ConnPoll, ConnAttach, ConnSendFiles, ...)
    +  xfer.bas       File transfers with XMODEM, XMODEM-1K, YMODEM, and ZMODEM, on a serial port or a connection, with their progress and questions (XferSendFile, XferStatus, XferBytes, XferAnswer, ...)
       prefs.bas      Settings files (INI): load one, read and change its settings, list the keys of a section, save it; DVX's own settings file, the screen mode it starts in, the names its choices are stored as and its default choices, and choices stored by names of your own (PrefsLoad, PrefsGet$, PrefsSet, PrefsKeyName$, PrefsSaveAs, PrefsDvxIni$, PrefsVideoWidth, PrefsChoiceLabel$, PrefsChoiceDefault, PrefsGetChoiceFrom, ...)
       resource.bas   Read and write DVX resource blocks (icons, text, binary data) in bundled files
       help.bas       Compile help source files and see the compiler's messages, and open the DVX help viewer, at a topic if you like (HelpCompile, HelpCompileMessages$, HelpView, HelpViewTopic; HelpView "" opens the DVX user guide)
    -  hlpfile.bas    Read compiled help files yourself, for a help viewer of your own: topics, table of contents, keyword index, full-text search, and each topic's headings, text, links and pictures; show a topic in a HelpPane control (HlpOpen, HlpTocTitle$, HlpIndexKeyword$, HlpSearch, HlpLoadTopic, ...)
    +  hlpfile.bas    Read compiled help files yourself, for a help viewer of your own: topics, table of contents, keyword index, full-text search, and each topic's headings, text, links, and pictures; show a topic in a HelpPane control (HlpOpen, HlpTocTitle$, HlpIndexKeyword$, HlpSearch, HlpLoadTopic, ...)
       net.bas        Network connections: TCP (TcpConnect, TcpListen, TcpSend, TcpRecv$, TcpPeer$, ...), UDP messages and name lookups, any bytes; the network settings and status (NetConfigLoad, NetConfigSet, NetConfigStore, NetRestart, NetStatus$, ...)
       print.bas      Printing: text with the page setup and pictures, with or without the Print dialog; a form; the Page Setup dialog; the printer and page settings; the printer ports (PrintModelLabel$, PrintSettingsStore, PrintPageSetting, PrintChoose, PrintForm, PrintPortStatus, ...)
       listhelp.bas   Lists and icon views you draw yourself (a UserControl, a PictureBox) that behave like ListBox and Program Manager: selection with Shift and Ctrl, the arrow keys in a list or a grid, Ctrl+A, Space, type-ahead, where a dragged item lands, and Program Manager's icon view rules -- its cells, the grid with a scroll bar, names on two lines, dragging and dropping (ListSelCreate, ListSelClick, ListSelNavigate, ListTypeAhead, ListIconMetric, ListIconLabel$, ...)
    -  security.bas   Security: random bytes for keys, a key exchange, a cipher, SHA-256 hashes, HMAC signatures, key making and a safe compare (SecRandom$, SecDhCreate, SecCipherCrypt$, SecSha256$, SecHmac$, SecHkdf$, ...)
    +  security.bas   Security: random bytes for keys, a key exchange, a cipher, SHA-256 hashes, HMAC signatures, key making, and a safe compare (SecRandom$, SecDhCreate, SecCipherCrypt$, SecSha256$, SecHmac$, SecHkdf$, ...)
       game.bas       Games: whether a key is being held down right now, forgetting held keys, frames FRAMEWAIT skipped, waiting for the screen's retrace (GameKeyDown, GameKeyClear, FrameMissed, GameWaitRetrace)
    -  joystick.bas   Joysticks: how many there are, their names, where each stick points and which buttons are pressed (JoyCount, JoyX, JoyY, JoyButton, ...); setting them up: calibration, dead zone, game port mode, keyboard joystick keys (JoySetCalibration, JoyCalibration, JoyRescan, JoyKeyIndex, JoySettingsStore, ...)
    -  sound.bas      Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...); sounds loaded once and played with volume, pan and pitch, sound made by the program as it goes, the FM synthesizer chip (SoundLoad, SoundPlay, SoundSetPitch, SoundCreate, SoundStreamOpen, SoundStreamWrite, FmAlloc, FmWrite, ...); DVX's own event sounds (SoundPlayEvent), master volume, sound driver and MIDI settings (SoundEventFile$, SoundSetMasterVolume, SoundSettingsStore, ...)
    -  music.bas      Music: play MOD, S3M, XM, IT and MIDI files, and play notes yourself (MusicLoad, MusicPlay, MusicStop, MusicNoteOn, MusicInfo$, ...); raw bytes to the MIDI port (MidiAcquire, MidiPut, MidiRelease, MidiPortInfo$)
    -  fileops.bas    Files and folders: folder listings for a file list, paths and names, the file dialogs' filter rule, attributes and the time a file was changed, sizes, free space and disk sizes, drive kinds, whether a folder can be written, the folder for scratch files, making a folder path, and copying, moving and deleting whole folders at once or as a job with progress, Cancel and questions (FileOpsList, FileOpsListRows$, FileOpsFilterMatch, FileOpsSetModified, FileOpsDriveType, FileOpsIsWritable, FileOpsMakePath, FileOpsJobCreate, FileOpsJobRunFor, ...)
    -  system.bas     What Control Panel changes, at once for the whole desktop: the screen mode, the system colors and themes, the wallpaper and the mouse (with DVX's limits and defaults, MOUSE_* and SYS_VIDEO_DEFAULT_*), and storing them in DVX's settings; the system font's character pictures; the mouse pointer and the Shift, Ctrl and Alt keys anywhere on the screen, moving the pointer, typing keys into the window in front, a picture of the whole screen, the memory (SysSetVideoMode, SysSetColor, SysLoadTheme, SysSetWallpaper, SysSetMouse, SysColorsStore, SysFontGlyphRow, SysMouseX, SysSendKey, SysScreenshot, SysMemoryFreeKb, ...)
    +  joystick.bas   Joysticks: how many there are, their names, where each stick points, and which buttons are pressed (JoyCount, JoyX, JoyY, JoyButton, ...); setting them up: calibration, dead zone, game port mode, keyboard joystick keys (JoySetCalibration, JoyCalibration, JoyRescan, JoyKeyIndex, JoySettingsStore, ...)
    +  sound.bas      Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...); sounds loaded once and played with volume, pan, and pitch, sound made by the program as it goes, the FM synthesizer chip (SoundLoad, SoundPlay, SoundSetPitch, SoundCreate, SoundStreamOpen, SoundStreamWrite, FmAlloc, FmWrite, ...); DVX's own event sounds (SoundPlayEvent), master volume, sound driver, and MIDI settings (SoundEventFile$, SoundSetMasterVolume, SoundSettingsStore, ...)
    +  music.bas      Music: play MOD, S3M, XM, IT, and MIDI files, and play notes yourself (MusicLoad, MusicPlay, MusicStop, MusicNoteOn, MusicInfo$, ...); raw bytes to the MIDI port (MidiAcquire, MidiPut, MidiRelease, MidiPortInfo$)
    +  fileops.bas    Files and folders: folder listings for a file list, paths and names, the file dialogs' filter rule, attributes and the time a file was changed, sizes, free space and disk sizes, drive kinds, whether a folder can be written, the folder for scratch files, making a folder path, and copying, moving, and deleting whole folders at once or as a job with progress, Cancel, and questions (FileOpsList, FileOpsListRows$, FileOpsFilterMatch, FileOpsSetModified, FileOpsDriveType, FileOpsIsWritable, FileOpsMakePath, FileOpsJobCreate, FileOpsJobRunFor, ...)
    +  system.bas     What Control Panel changes, at once for the whole desktop: the screen mode, the system colors and themes, the wallpaper, text smoothing, and the mouse (with DVX's limits and defaults, MOUSE_* and SYS_VIDEO_DEFAULT_*), and storing them in DVX's settings; the system font's character pictures; the mouse pointer and the Shift, Ctrl, and Alt keys anywhere on the screen, moving the pointer, typing keys into the window in front, a picture of the whole screen, and the memory (SysSetVideoMode, SysSetColor, SysLoadTheme, SysSetWallpaper, SysSetTextSmoothing, SysSetMouse, SysColorsStore, SysFontGlyphRow, SysMouseX, SysSendKey, SysScreenshot, SysMemoryFreeKb, ...)
    +  font.bas       Fonts: the font families DVX has, the sizes each comes in, whether one is fixed-pitch, and how wide text and how tall a line is in a font (FontFamilyCount, FontFamilyName$, FontFamilyFixed, FontSizeCount, FontSizeAt, FontNearestSize, FontTextWidth, FontLineHeight, FontAscent, FontRescan)
    +  richtext.bas   RTF without a control: an RTF document as plain text and plain text as RTF (RtfToText$, TextToRtf$); a RichEdit control reads and writes RTF itself (TextRTF, SelRTF, LoadFile, SaveFile)
       layout.bas     Control sizes in characters or in percent of the container instead of pixels (SizeChars, SizePercent)
    -  compiler.bas   The BASIC compiler: compile a project or files you give it, find compile errors in their files, list a program's procedures and the language's keywords, build an .app or a control file; put a project's UserControls on a form designer's toolbox; read source text as the compiler does (tokens, code or string or comment, renaming a control in code, WithEvents variables, class Events), F1 help topics; open, change and save project files and UserControl headers (CompSourceFromProject, CompSourceAdd, CompCompile, CompError$, CompMapFile$, CompProcList$, CompBuildApp, CompDesignControls, CompTokens$, CompRenameInCode$, CompHelpTopic$, ProjOpen, ProjSave, ProjCtlSetHeader$, ...)
    +  compiler.bas   The BASIC compiler: compile a project or files you give it, find compile errors in their files, list a program's procedures and the language's keywords, build an .app or a control file; put a project's UserControls on a form designer's toolbox; read source text as the compiler does (tokens, code or string or comment, renaming a control in code, WithEvents variables, class Events), F1 help topics; open, change, and save project files and UserControl headers (CompSourceFromProject, CompSourceAdd, CompCompile, CompError$, CompMapFile$, CompProcList$, CompBuildApp, CompDesignControls, CompTokens$, CompRenameInCode$, CompHelpTopic$, ProjOpen, ProjSave, ProjCtlSetHeader$, ...)
       debug.bas      Run a compiled program inside yours and debug it: breakpoints, stepping, its variables, expressions, Immediate-window lines, its PRINT output (RunCreate, RunStart, RunPoll, DbgSetBreakpoints, DbgStepOver, DbgLocals$, DbgEval$, DbgExec$, ...)
    -  designer.bas   Form designers: the control types, whether each holds other controls and can be placed, the names of new controls, default events, toolbox tooltips and icons, and each type's properties (with their types and value names), methods and events (with each event's parameters); the names in .frm and class module text; .frm files as data to read, edit and write, controls copied and pasted as text, and a loaded form written back as .frm text (CtlTypeCount, CtlTypeName$, CtlTypeFlags, CtlPropCount, CtlPropName$, CtlPropType, CtlEventList$, CtlEventParams$, CtlTypeEventParams$, FrmFormName$, FrmControlNames$, ClsClassName$, FrmOpen, FrmText$, FrmCtlAdd, FrmCtlText$, FrmCtlPaste, FrmMenuAdd, FormText$, ...)
    -  shell.bas      DVX itself, everything Program Manager and Task Manager do: start programs and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close or end it), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, this program's settings folder, the DVX log; Program Manager's program groups with their items, shortcut keys and icons (ShellRun, ShellRunProgram, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellConfigFile$, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...)
    +  designer.bas   Form designers: the control types, whether each holds other controls and can be placed, the names of new controls, default events, toolbox tooltips and icons, and each type's properties (with their types and value names), methods, and events (with each event's parameters); the names in .frm and class module text; .frm files as data to read, edit, and write, controls copied and pasted as text, and a loaded form written back as .frm text (CtlTypeCount, CtlTypeName$, CtlTypeFlags, CtlPropCount, CtlPropName$, CtlPropType, CtlEventList$, CtlEventParams$, CtlTypeEventParams$, FrmFormName$, FrmControlNames$, ClsClassName$, FrmOpen, FrmText$, FrmCtlAdd, FrmCtlText$, FrmCtlPaste, FrmMenuAdd, FormText$, ...)
    +  shell.bas      DVX itself, everything Program Manager and Task Manager do: start programs and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close, or end it), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, this program's settings folder, the DVX log; Program Manager's program groups with their items, shortcut keys, and icons (ShellRun, ShellRunProgram, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellConfigFile$, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...)
     .endtable
     
     .h2 Using an Include Library
    @@ -5091,7 +5110,7 @@ On Error GoTo 0
     
     .h2 Message Boxes with Your Own Buttons
     
    -MsgBox offers fixed sets of buttons (OK, Yes and No, ...). With commdlg.bas in the project, DlgButtons(title, message, buttons, icon) shows a message box with the buttons you list, separated by |, with & before each button's access key. It returns the index of the button chosen: 0 for the first, which Enter presses. Esc and closing the box choose the last button, so make the last one the choice that changes nothing. icon is vbQuestion, vbInformation, vbExclamation, vbCritical or 0. Up to six buttons; -1 is returned when the list is empty or longer.
    +MsgBox offers fixed sets of buttons (OK, Yes and No, ...). With commdlg.bas in the project, DlgButtons(title, message, buttons, icon) shows a message box with the buttons you list, separated by |, with & before each button's access key. It returns the index of the button chosen: 0 for the first, which Enter presses. Esc and closing the box choose the last button, so make the last one the choice that changes nothing. icon is vbQuestion, vbInformation, vbExclamation, vbCritical, or 0. Up to six buttons; -1 is returned when the list is empty or longer.
     
     .code
     Select Case DlgButtons("Copy", "notes.txt already exists. Replace it?", "&Yes|&No|Yes to &All|&Cancel", vbQuestion)
    @@ -5106,7 +5125,7 @@ End Select
     
     .h2 Color Dialog
     
    -With commdlg.bas in the project, DlgColor(title, defaultColor) shows the color dialog: a palette of basic colors, Red, Green and Blue sliders with number boxes, and a preview. It starts at defaultColor and returns the color chosen, in the form RGB makes and ForeColor and BackColor use, or -1 when the user cancels.
    +With commdlg.bas in the project, DlgColor(title, defaultColor) shows the color dialog: a palette of basic colors, Red, Green, and Blue sliders with number boxes, and a preview. It starts at defaultColor and returns the color chosen, in the form RGB makes and ForeColor and BackColor use, or -1 when the user cancels.
     
     .code
     Dim c As Long
    @@ -5117,6 +5136,27 @@ If c <> -1 Then
     End If
     .endcode
     
    +.h2 Font Dialog
    +
    +With commdlg.bas in the project, DlgFont(title, family, size, style, flags) shows the font dialog: the font families, the styles and sizes each comes in, Underline and Strikeout boxes, and a sample. It starts at the font named by family, size (in pixels), and style, and returns True when the user chooses OK. DlgFontName$(), DlgFontSize(), and DlgFontStyle() then give the font chosen; after Cancel they give the font the dialog started at. style adds up FONT_BOLD, FONT_ITALIC, FONT_UNDERLINE, and FONT_STRIKE, the constants of font.bas (add it to the project too, or use 1, 2, 4, and 8). With flags DLG_FONT_FIXED the dialog lists only the fixed-pitch families, for text that must line up in columns; with 0 it lists them all. The dialog's Licence button shows the chosen font's notice and licence. The answers fit a control's or a form's Font properties (see Fonts).
    +
    +.code
    +' With commdlg.bas and font.bas added to the project
    +Dim style As Long
    +
    +If Label1.FontBold Then style = style + FONT_BOLD
    +If Label1.FontItalic Then style = style + FONT_ITALIC
    +
    +If DlgFont("Label Font", Label1.FontName, Label1.FontSize, style, 0) Then
    +    Label1.FontName = DlgFontName$()
    +    Label1.FontSize = DlgFontSize()
    +    Label1.FontBold = (DlgFontStyle() And FONT_BOLD) <> 0
    +    Label1.FontItalic = (DlgFontStyle() And FONT_ITALIC) <> 0
    +    Label1.FontUnderline = (DlgFontStyle() And FONT_UNDERLINE) <> 0
    +    Label1.FontStrikethru = (DlgFontStyle() And FONT_STRIKE) <> 0
    +End If
    +.endcode
    +
     .h2 Number Input Dialog
     
     With commdlg.bas in the project, DlgIntInput(title, prompt, default, min, max) asks for a whole number with a spin box, and DlgIntInputStep(title, prompt, default, min, max, step) does the same with the arrows moving by step. Both return the number entered, or default when the user cancels; DlgInputCancelled() (True after a Cancel, False after OK) tells the two apart, as it does for DlgInputBox$.
    @@ -5131,7 +5171,7 @@ End If
     
     .h2 Resources in Files
     
    -With resource.bas in the project, a program reads and writes the named resources DVX keeps at the end of program, widget and library files: icons, text and any bytes. ResOpen, ResCount, ResName$, ResType and ResSize list them; ResGetText$ and ResAddText read and write text; ResAddFile and ResExtract copy a resource from or to a file; ResRemove takes one out. ResGetData$(file, name) gives every byte of a resource as a String (CHR$(0) included), and ResAddData file, name, type, bytes adds or replaces a resource of any bytes (type RES_TYPE_BINARY, RES_TYPE_TEXT or RES_TYPE_ICON). RES_NAME_ICON ("icon32") and RES_NAME_WIDGET_ICON ("icon24") are the names DVX gives a program's and a widget's icon; a name is at most RES_NAME_MAX - 1 characters.
    +With resource.bas in the project, a program reads and writes the named resources DVX keeps at the end of program, widget, and library files: icons, text, and any bytes. ResOpen, ResCount, ResName$, ResType, and ResSize list them; ResGetText$ and ResAddText read and write text; ResAddFile and ResExtract copy a resource from or to a file; ResRemove takes one out. ResGetData$(file, name) gives every byte of a resource as a String (CHR$(0) included), and ResAddData file, name, type, bytes adds or replaces a resource of any bytes (type RES_TYPE_BINARY, RES_TYPE_TEXT, RES_TYPE_ICON, or RES_TYPE_FONT, a .DFN font the program carries; see Fonts). RES_NAME_ICON ("icon32") and RES_NAME_WIDGET_ICON ("icon24") are the names DVX gives a program's and a widget's icon; a name is at most RES_NAME_MAX - 1 characters.
     
     .code
     Dim settings As String
    @@ -5163,7 +5203,7 @@ End If
     
     After the handshake, data that arrives unencrypted is ignored unless that channel is opened for it with CommSetPlain link, channel, True.
     
    -CommHandshake waits until the key exchange is done. CommHandshakeStep(link, passphrase) does one step of it and returns at once, so a program can keep working: call it from a Timer until it returns something other than COMM_PENDING -- COMM_OK when the link is ready, else a COMM_ERR_ code. CommLastError(link) gives the COMM_ code of the last CommHandshake, CommHandshakeStep, CommSend, CommSetPlain or CommPoll, which tells a wrong passphrase (COMM_ERR_AUTH) from a line that does not answer (COMM_ERR_TIMEOUT). CommDropped(link) counts arriving packets thrown away because they were tampered with or came unencrypted where that is refused. Channels are numbered 0 to COMM_MAX_CHANNEL.
    +CommHandshake waits until the key exchange is done. CommHandshakeStep(link, passphrase) does one step of it and returns at once, so a program can keep working: call it from a Timer until it returns something other than COMM_PENDING -- COMM_OK when the link is ready, else a COMM_ERR_ code. CommLastError(link) gives the COMM_ code of the last CommHandshake, CommHandshakeStep, CommSend, CommSetPlain, or CommPoll, which tells a wrong passphrase (COMM_ERR_AUTH) from a line that does not answer (COMM_ERR_TIMEOUT). CommDropped(link) counts arriving packets thrown away because they were tampered with or came unencrypted where that is refused. Channels are numbered 0 to COMM_MAX_CHANNEL.
     
     .code
     Sub Timer1_Timer ()
    @@ -5191,7 +5231,7 @@ With comm.bas, SerOpen returns False when a port cannot be opened; SerLastError(
       Function                                            Description
       --------                                            -----------
       SerSet(com, baud, dataBits, parity, stopBits, handshake)  Change the settings of an open port (SerOpen's arguments). True when done.
    -  SerGetBaud(com), SerGetDataBits(com), SerGetParity$(com), SerGetStopBits(com), SerGetHandshake(com)  The settings of an open port; parity is "N", "E", "O", "M" or "S". When the port is not open the numbers are an SER_ERR_ code and the parity "".
    +  SerGetBaud(com), SerGetDataBits(com), SerGetParity$(com), SerGetStopBits(com), SerGetHandshake(com)  The settings of an open port; parity is "N", "E", "O", "M", or "S". When the port is not open the numbers are an SER_ERR_ code and the parity "".
       SerTxPending(com), SerFlushTx com                   Bytes written but not sent yet; throw them away.
       SerSetBreak com, state                              Send a break: True holds the line in the break state, False releases it (hold it a quarter to half a second).
       SerGetRi(com)                                       True while the other end rings.
    @@ -5200,7 +5240,7 @@ With comm.bas, SerOpen returns False when a port cannot be opened; SerLastError(
     
     .h2 Printing
     
    -With print.bas in the project, a program prints like the programs that come with DVX. PrintTextDialog shows the Print dialog (printer, copies, pages) and prints the text on pages laid out by the page setup: margins, a header and a footer with the document name, date and page number. PrintPageSetup shows the Page Setup dialog, which the user's choices are saved from for every program. PrintPictureDialog prints a picture file scaled to the page, on printer models that print pictures (PrintCanPrintPictures says whether the chosen one does). PrintText and PrintPicture print without asking first. Each returns True when it printed.
    +With print.bas in the project, a program prints like the programs that come with DVX. PrintTextDialog shows the Print dialog (printer, copies, pages) and prints the text on pages laid out by the page setup: margins, a header, and a footer with the document name, date, and page number. PrintPageSetup shows the Page Setup dialog, which the user's choices are saved from for every program. PrintPictureDialog prints a picture file scaled to the page, on printer models that print pictures (PrintCanPrintPictures says whether the chosen one does). PrintText and PrintPicture print without asking first. Each returns True when it printed.
     
     .code
     Sub mnuPrint_Click ()
    @@ -5221,7 +5261,7 @@ A program that prints its own way has more:
       --------                                            -----------
       PrintPageCount(text)                                How many pages text takes with the saved page setup and printer.
       PrintTextPages(text, title, firstPage, lastPage, copies)  Print text without asking: those pages (0 and 0 for all), copies times.
    -  PrintChoose(title, pageCount)                       Show the Print dialog only (the page range when pageCount is above 0). True when the user chose Print; then PrintChoiceDest, PrintChoiceFile$, PrintChoiceCopies, PrintChoiceFirst and PrintChoiceLast say what was chosen (pages 0 and 0: all).
    +  PrintChoose(title, pageCount)                       Show the Print dialog only (the page range when pageCount is above 0). True when the user chose Print; then PrintChoiceDest, PrintChoiceFile$, PrintChoiceCopies, PrintChoiceFirst, and PrintChoiceLast say what was chosen (pages 0 and 0: all).
       PrintForm(formName) / PrintFormDialog(formName)     Print what a loaded form shows, scaled to the page (a printer model that prints pictures); the second shows the Print dialog first.
       PrintPortPresent(port), PrintPortStatus(port)       Whether printer port LPTn (1 to 3) exists, and its state: PRINT_STATUS_ values added up (PRINT_STATUS_PAPER_OUT, PRINT_STATUS_SELECTED, PRINT_STATUS_NOT_BUSY, ...).
       PrintStatusProblem$(status, afterWrite, where)      What is wrong with a printer in that state, in words ("" when nothing is), for printer where such as "LPT1".
    @@ -5245,13 +5285,13 @@ The page setup can be read and changed without the Page Setup dialog. Like the p
       Function                                            Description
       --------                                            -----------
       PrintPageSetting(prefs, field)                      A number of the page setup: "Lines" (lines per page, 0 = the printer's usual length), "Columns", "MarginTop", "MarginBottom", "MarginLeft", "MarginRight", "TabWidth", or "Wrap" (True: long lines wrap at a space); -1 for an unknown field.
    -  PrintPageText$(prefs, field)                        "Header" or "Footer". In them &f is the document name, &p the page, &n the number of pages, &d the date, &t the time, && an ampersand; &l, &c and &r put what follows left, centred or right.
    +  PrintPageText$(prefs, field)                        "Header" or "Footer". In them &f is the document name, &p the page, &n the number of pages, &d the date, &t the time, && an ampersand; &l, &c, and &r put what follows left, centred, or right.
       PrintPageSetupProblem$(lines, columns, marginTop, marginBottom, marginLeft, marginRight, tabWidth, wrap, header, footer)  "" when this page setup can be used, else why not.
       PrintPageSettingsStore(prefs, lines, columns, marginTop, marginBottom, marginLeft, marginRight, tabWidth, wrap, header, footer)  Put a page setup into the settings (then PrefsSave). False for one PrintPageSetupProblem$ objects to.
       PrintPageLines(prefs, model)                        Lines per page the setup gives on printer model.
     .endtable
     
    -The limits are CONSTs: PRINT_LINES_MIN to PRINT_LINES_MAX, PRINT_COLUMNS_MIN to PRINT_COLUMNS_MAX, PRINT_TAB_MAX, PRINT_COPIES_MAX, with the defaults PRINT_COLUMNS_DEFAULT, PRINT_MARGIN_TB_DEFAULT, PRINT_MARGIN_LR_DEFAULT and PRINT_TAB_DEFAULT.
    +The limits are CONSTs: PRINT_LINES_MIN to PRINT_LINES_MAX, PRINT_COLUMNS_MIN to PRINT_COLUMNS_MAX, PRINT_TAB_MAX, PRINT_COPIES_MAX, with the defaults PRINT_COLUMNS_DEFAULT, PRINT_MARGIN_TB_DEFAULT, PRINT_MARGIN_LR_DEFAULT, and PRINT_TAB_DEFAULT.
     
     A program can also change the printer settings the way the Control Panel's Printer page does. They are kept in DVX's settings file, so these calls take a handle from prefs.bas (add prefs.bas to the project too). Printer models and destinations are numbered from 0.
     
    @@ -5290,15 +5330,15 @@ End Sub
     
     .h2 Files and Folders
     
    -With fileops.bas in the project, a program can show and change what is on the disks. FileOpsList(folder, filter) reads a folder once -- its folders and the files matching filter -- and returns a listing (a number; FileOpsListFree frees it). FileOpsListRows$ gives the whole listing as name, size, type and date columns, ready for a ListView's SetRows, so even a folder of hundreds of files fills the list at once; FileOpsListSort re-sorts it by name, size, type or date with folders first. FileOpsListName$, FileOpsListIsFolder and FileOpsListSize read single entries, and FileOpsListFind finds one by name.
    +With fileops.bas in the project, a program can show and change what is on the disks. FileOpsList(folder, filter) reads a folder once -- its folders and the files matching filter -- and returns a listing (a number; FileOpsListFree frees it). FileOpsListRows$ gives the whole listing as name, size, type, and date columns, ready for a ListView's SetRows, so even a folder of hundreds of files fills the list at once; FileOpsListSort re-sorts it by name, size, type, or date with folders first. FileOpsListName$, FileOpsListIsFolder, and FileOpsListSize read single entries, and FileOpsListFind finds one by name.
     
    -Paths: FileOpsPathNormalize$ (the full path of an existing folder, or ""), FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (True for a path that is complete by itself, such as "C:\DOCS", False for one that counts from the current folder), FileOpsPathIsInside(path, folder) (True when path is folder itself or anywhere inside it: check it before copying or moving a folder, so it is never copied into itself), FileOpsPathSep$ (the character between folder names, "\" on DOS), FileOpsPathNative$ (a path written with that character, for showing it: "C:/DOCS/A.TXT" becomes "C:\DOCS\A.TXT"), FileOpsBaseName$ and FileOpsExtension$. Every one of them takes / and \ alike and gives back \. FileOpsFilterMatch(itemName, filter) tells whether a name matches a filter the way the file dialogs match it: wildcards * and ?, several patterns separated by ; such as "*.bmp;*.png", case not mattering.
    +Paths: FileOpsPathNormalize$ (the full path of an existing folder, or ""), FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (True for a path that is complete by itself, such as "C:\DOCS", False for one that counts from the current folder), FileOpsPathIsInside(path, folder) (True when path is folder itself or anywhere inside it: check it before copying or moving a folder, so it is never copied into itself), FileOpsPathSep$ (the character between folder names, "\" on DOS), FileOpsPathNative$ (a path written with that character, for showing it: "C:/DOCS/A.TXT" becomes "C:\DOCS\A.TXT"), FileOpsBaseName$, and FileOpsExtension$. Every one of them takes / and \ alike and gives back \. FileOpsFilterMatch(itemName, filter) tells whether a name matches a filter the way the file dialogs match it: wildcards * and ?, several patterns separated by ; such as "*.bmp;*.png", case not mattering.
     
    -Files: FileOpsMakeDir and FileOpsRename (FileOpsError$ says why one failed), FileOpsTempPath$ and FileOpsReplaceFile (save safely: write the new file to FileOpsTempPath$(path$), then FileOpsReplaceFile swaps it in; the old file comes back if that fails), FileOpsValidName(folder, itemName) (whether a name can be used for a new file or folder in folder, by the rules of its disk; FileOpsError$ says why not), FileOpsValidLongName and FileOpsValidShortName (the long-name or the 8.3 rules by themselves, whatever the disk takes), FileOpsShortPath$ (the short 8.3 form of an existing path, for an old program that cannot take long names or blanks), FileOpsMakePath (a folder and every missing folder above it, which MKDIR does not make), FileOpsIsFolder, FileOpsGetAttr and FileOpsSetAttr (the FILEOPS_ATTR_ bits), FileOpsModified$ and FileOpsSetModified (when a file was last changed, read and set in the form "YYYY-MM-DD HH:MM"; FileOpsSetModified returns False for a date that does not exist), FileOpsTreeSize with FileOpsTreeBytes, FileOpsTreeFiles and FileOpsTreeFolders (the size of a whole folder) and FileOpsFormatSize$.
    +Files: FileOpsMakeDir and FileOpsRename (FileOpsError$ says why one failed), FileOpsTempPath$ and FileOpsReplaceFile (save safely: write the new file to FileOpsTempPath$(path$), then FileOpsReplaceFile swaps it in; the old file comes back if that fails), FileOpsValidName(folder, itemName) (whether a name can be used for a new file or folder in folder, by the rules of its disk; FileOpsError$ says why not), FileOpsValidLongName and FileOpsValidShortName (the long-name or the 8.3 rules by themselves, whatever the disk takes), FileOpsShortPath$ (the short 8.3 form of an existing path, for an old program that cannot take long names or blanks), FileOpsMakePath (a folder and every missing folder above it, which MKDIR does not make), FileOpsIsFolder, FileOpsGetAttr, and FileOpsSetAttr (the FILEOPS_ATTR_ bits), FileOpsModified$ and FileOpsSetModified (when a file was last changed, read and set in the form "YYYY-MM-DD HH:MM"; FileOpsSetModified returns False for a date that does not exist), FileOpsTreeSize with FileOpsTreeBytes, FileOpsTreeFiles, and FileOpsTreeFolders (the size of a whole folder), and FileOpsFormatSize$.
     
    -Drives: FileOpsDriveList$ (the drive letters), FileOpsDriveType (FILEOPS_DRIVE_FIXED for a hard disk, FILEOPS_DRIVE_REMOVABLE for a floppy disk drive, FILEOPS_DRIVE_REMOTE for a network drive or a CD-ROM; asking never makes a drive spin up), FileOpsBootDrive$ (the drive the computer started from), FileOpsFreeBytes, FileOpsFreeKb and FileOpsTotalBytes (the size of the disk; right for disks past 2 GB too), FileOpsLongNames(path) (True when the disk holding path takes long file names: up to 255 characters, with spaces and several dots; otherwise names are 8.3). FileOpsIsWritable(folder) is False where files cannot be made (a CD-ROM, a write-protected floppy), and FileOpsReadOnlySystem() is True when DVX itself runs from such a disk, so settings cannot be saved in its folder. FileOpsTempDir$() is the folder for scratch files ("" when there is none).
    +Drives: FileOpsDriveList$ (the drive letters), FileOpsDriveType (FILEOPS_DRIVE_FIXED for a hard disk, FILEOPS_DRIVE_REMOVABLE for a floppy disk drive, FILEOPS_DRIVE_REMOTE for a network drive or a CD-ROM; asking never makes a drive spin up), FileOpsBootDrive$ (the drive the computer started from), FileOpsFreeBytes, FileOpsFreeKb, and FileOpsTotalBytes (the size of the disk; right for disks past 2 GB too), FileOpsLongNames(path) (True when the disk holding path takes long file names: up to 255 characters, with spaces and several dots; otherwise names are 8.3). FileOpsIsWritable(folder) is False where files cannot be made (a CD-ROM, a write-protected floppy), and FileOpsReadOnlySystem() is True when DVX itself runs from such a disk, so settings cannot be saved in its folder. FileOpsTempDir$() is the folder for scratch files ("" when there is none).
     
    -FileOpsCopyTree, FileOpsMoveTree and FileOpsDeleteTree do a whole folder at once. For a long copy, move or delete the program stays usable when it runs the work as a job, a little at a time from a Timer: FileOpsJobCreate, then FileOpsJobAddSource for each file or folder, then FileOpsJobRunFor from the timer until it returns FILEOPS_STEP_DONE or FILEOPS_STEP_CANCELLED. When it returns FILEOPS_STEP_ASK (a file already exists, a file is read-only) or FILEOPS_STEP_ERROR, show FileOpsJobMessage$ and pass the user's choice to FileOpsJobAnswer. FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. Files copied from a CD-ROM are read-only; FileOpsJobClearAttr job, FILEOPS_ATTR_READONLY before the job runs makes every copy changeable (FILEOPS_ATTR_EDITABLE clears all four attribute bits). Whatever a program leaves open is freed when it ends.
    +FileOpsCopyTree, FileOpsMoveTree, and FileOpsDeleteTree do a whole folder at once. For a long copy, move, or delete the program stays usable when it runs the work as a job, a little at a time from a Timer: FileOpsJobCreate, then FileOpsJobAddSource for each file or folder, then FileOpsJobRunFor from the timer until it returns FILEOPS_STEP_DONE or FILEOPS_STEP_CANCELLED. When it returns FILEOPS_STEP_ASK (a file already exists, a file is read-only) or FILEOPS_STEP_ERROR, show FileOpsJobMessage$ and pass the user's choice to FileOpsJobAnswer. FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. Files copied from a CD-ROM are read-only; FileOpsJobClearAttr job, FILEOPS_ATTR_READONLY before the job runs makes every copy changeable (FILEOPS_ATTR_EDITABLE clears all four attribute bits). Whatever a program leaves open is freed when it ends.
     
     .code
     Dim job As Long
    @@ -5368,9 +5408,9 @@ Command lines. ShellCommandProgram$(commandLine) and ShellCommandArgs$(commandLi
     
     Keys for every program. HotkeyAdd(keyText) makes a key such as "Ctrl+Alt+E" work whichever program is in front and returns its id (0 when it is not a key, needs Ctrl or Alt, or another program uses it). The program asks for the keys pressed from a Timer: HotkeyNext() returns the id of the oldest one not asked for yet, 0 when there is none. HotkeyRemove id stops one; the rest stop when the program ends. HotkeyText$(keyText) writes a key the way DVX does ("ctrl+alt+e" gives "Ctrl+Alt+E"; "" when it is not a key), handy for checking what the user typed. ShellItemKeyValid(keyText) is Program Manager's rule for an item's shortcut: Ctrl together with Alt or Shift, so it never takes a key a program uses itself ("" for no shortcut is valid too).
     
    -This program. ShellAppPath$ is this program's own path (ShellRun ShellAppPath$(), "" starts another copy), and ShellInstanceCount says how many copies are running, for example to place each new window a little lower and to the right. ShellSetPriority(priority) gives this program more or less of the computer's time while other programs run too: SHELL_PRIORITY_LOW, SHELL_PRIORITY_NORMAL (what every program starts with), SHELL_PRIORITY_HIGH or any number between; ShellPriority() reads it. A program busy with long work leaves the others room at low priority. A program allows more than one copy when Project Properties has "Allow more than one copy to run at once" ticked. ShellConfigFile$(fileName) is the full path of fileName in this program's settings folder -- where DVX's own programs keep their settings, the same for every copy of the program -- and makes the folder when it is missing; keep a desktop's groups there with prefs.bas. ShellLog text writes a line to the DVX log. ShellSetDesktop program makes a program the desktop DVX starts with the next time ("" for Program Manager again); when the desktop ends, DVX ends. ShellSetWelcome program sets the program DVX starts after the desktop the next time it starts ("" for none).
    +This program. ShellAppPath$ is this program's own path (ShellRun ShellAppPath$(), "" starts another copy), and ShellInstanceCount says how many copies are running, for example to place each new window a little lower and to the right. ShellSetPriority(priority) gives this program more or less of the computer's time while other programs run too: SHELL_PRIORITY_LOW, SHELL_PRIORITY_NORMAL (what every program starts with), SHELL_PRIORITY_HIGH, or any number between; ShellPriority() reads it. A program busy with long work leaves the others room at low priority. A program allows more than one copy when Project Properties has "Allow more than one copy to run at once" ticked. ShellConfigFile$(fileName) is the full path of fileName in this program's settings folder -- where DVX's own programs keep their settings, the same for every copy of the program -- and makes the folder when it is missing; keep a desktop's groups there with prefs.bas. ShellLog text writes a line to the DVX log. ShellSetDesktop program makes a program the desktop DVX starts with the next time ("" for Program Manager again); when the desktop ends, DVX ends. ShellSetWelcome program sets the program DVX starts after the desktop the next time it starts ("" for none).
     
    -Program groups. Program Manager keeps its groups in PROGMAN.INI; a desktop written in BASIC uses the same model and file format. ShellGroupsLoad(path) opens a groups file (a file not saved yet has the groups Main and Startup) and returns a handle, ShellGroupsSave(groups, path) writes it and ShellGroupsFree frees it. Groups and items are numbered from 0; group 0 is Installed Programs, which ShellGroupsScan(groups, skipPath) fills with every installed program and which is never saved. ShellGroupsChanged tells cheaply whether a program was installed or removed since. ShellGroupCount, ShellGroupName$, ShellGroupFind, ShellGroupAdd, ShellGroupRename, ShellGroupDelete and ShellGroupNameValid manage the groups. ShellItemCount and ShellItemAdd(groups, group, name, program) list and add items; ShellItemGet$ and ShellItemSet read and change an item's fields by name ("name", "program", "args", "folder", "icon", "key", "minimized", "col", "row", "description"); ShellItemDelete, ShellItemCopy, ShellItemMove, ShellItemReorder and ShellItemPlace rearrange them, ShellGroupArrange and ShellGroupPlaceUnplaced lay out an icon grid, and ShellItemMatches is Program Manager's search. ShellGroupsGet and ShellGroupsSet read and set the options ("view": SHELL_VIEW_LARGE, SHELL_VIEW_SMALL or SHELL_VIEW_LIST; "group", the group shown; "autoArrange", "minimizeOnRun", "restoreAlone"), and ShellGroupsHistory$ / ShellGroupsHistoryAdd keep the Run history (what ShellRunDialog$ takes). ShellItemIcon$(iconFile, program) is the file an item's icon comes from -- its icon file, else its program or picture, else the program that opens the document -- ready for LOADPICTURE.
    +Program groups. Program Manager keeps its groups in PROGMAN.INI; a desktop written in BASIC uses the same model and file format. ShellGroupsLoad(path) opens a groups file (a file not saved yet has the groups Main and Startup) and returns a handle, ShellGroupsSave(groups, path) writes it, and ShellGroupsFree frees it. Groups and items are numbered from 0; group 0 is Installed Programs, which ShellGroupsScan(groups, skipPath) fills with every installed program and which is never saved. ShellGroupsChanged tells cheaply whether a program was installed or removed since. ShellGroupCount, ShellGroupName$, ShellGroupFind, ShellGroupAdd, ShellGroupRename, ShellGroupDelete, and ShellGroupNameValid manage the groups. ShellItemCount and ShellItemAdd(groups, group, name, program) list and add items; ShellItemGet$ and ShellItemSet read and change an item's fields by name ("name", "program", "args", "folder", "icon", "key", "minimized", "col", "row", "description"); ShellItemDelete, ShellItemCopy, ShellItemMove, ShellItemReorder, and ShellItemPlace rearrange them, ShellGroupArrange and ShellGroupPlaceUnplaced lay out an icon grid, and ShellItemMatches is Program Manager's search. ShellGroupsGet and ShellGroupsSet read and set the options ("view": SHELL_VIEW_LARGE, SHELL_VIEW_SMALL, or SHELL_VIEW_LIST; "group", the group shown; "autoArrange", "minimizeOnRun", "restoreAlone"), and ShellGroupsHistory$ / ShellGroupsHistoryAdd keep the Run history (what ShellRunDialog$ takes). ShellItemIcon$(iconFile, program) is the file an item's icon comes from -- its icon file, else its program or picture, else the program that opens the document -- ready for LOADPICTURE.
     
     .code
     Dim groups As Long, main As Long, i As Long
    @@ -5413,21 +5453,23 @@ End Sub
     
     With system.bas in the project, a program changes what Control Panel changes, at once for the whole desktop. Add prefs.bas too: the settings are saved through a prefs.bas handle.
     
    -Screen mode: SysVideoModeCount() says how many modes the display offers (numbered from 0), and SysVideoModeWidth, SysVideoModeHeight and SysVideoModeBpp (bits per pixel) describe each. SysSetVideoMode(wide, high, bpp) changes the mode and keeps every window; it returns False when the mode could not be set (the old one stays), also while a program has the whole screen to itself. A mode the monitor cannot show leaves the user unable to answer, so do as Control Panel does: ask "Keep this mode?" and go back to the old mode by itself after some seconds. Screen.Width, Screen.Height and Screen.BitsPerPixel tell the mode in use, and SYS_VIDEO_DEFAULT_WIDTH, SYS_VIDEO_DEFAULT_HEIGHT and SYS_VIDEO_DEFAULT_BPP the mode DVX starts in when its settings name none. SysScreenshot(path) saves the whole screen as a picture file (BMP when path ends in .bmp, PNG otherwise) and returns True when it was written.
    +Screen mode: SysVideoModeCount() says how many modes the display offers (numbered from 0), and SysVideoModeWidth, SysVideoModeHeight, and SysVideoModeBpp (bits per pixel) describe each. SysSetVideoMode(wide, high, bpp) changes the mode and keeps every window; it returns False when the mode could not be set (the old one stays), also while a program has the whole screen to itself. A mode the monitor cannot show leaves the user unable to answer, so do as Control Panel does: ask "Keep this mode?" and go back to the old mode by itself after some seconds. Screen.Width, Screen.Height, and Screen.BitsPerPixel tell the mode in use, and SYS_VIDEO_DEFAULT_WIDTH, SYS_VIDEO_DEFAULT_HEIGHT, and SYS_VIDEO_DEFAULT_BPP the mode DVX starts in when its settings name none. SysScreenshot(path) saves the whole screen as a picture file (BMP when path ends in .bmp, PNG otherwise) and returns True when it was written.
     
     Colors: the system colors are numbered from 0 to SysColorCount() - 1. SysColorLabel$(id) is the text to show for one ("Desktop", "Window Face" ...), SysColorName$(id) the name it is stored as, and SysColorId(colorName) the id of a name (-1 for none) -- to draw in the user's colors, for example SysGetColor(SysColorId("desktop")). SysGetColor and SysSetColor read and set a color as an RGB() value; each SysSetColor redraws the whole screen. SysResetColors goes back to DVX's own colors. SysLoadTheme(path) and SysSaveTheme(path) read and write a theme file (.THM, kept in CONFIG\THEMES); both return True when they worked.
     
    -Wallpaper: SysSetWallpaper(path) shows a BMP, JPG, PNG or GIF image behind the desktop ("" for none) and returns False when the image could not be loaded (the old one stays). SysSetWallpaperMode mode says how it fills the screen, a choice of PREFS_CHOICE_WALLPAPER (Stretch, Tile or Center; see Settings Files). SysWallpaper$() and SysWallpaperMode() tell what is in use.
    +Wallpaper: SysSetWallpaper(path) shows a BMP, JPG, PNG, or GIF image behind the desktop ("" for none) and returns False when the image could not be loaded (the old one stays). SysSetWallpaperMode mode says how it fills the screen, a choice of PREFS_CHOICE_WALLPAPER (Stretch, Tile, or Center; see Settings Files). SysWallpaper$() and SysWallpaperMode() tell what is in use.
     
    -Mouse: SysSetMouse wheelDir, dblClickMs, accel, mickeys, wheelStep applies a whole mouse setup at once: the scroll wheel's direction (1 or -1, the values of PREFS_CHOICE_WHEEL), the double-click time in milliseconds, the acceleration (a value of PREFS_CHOICE_ACCEL), how far the mouse moves for 8 pixels of pointer movement (8 is normal, higher is slower) and the lines scrolled per wheel notch (1 to 10). An accel or mickeys of 0 leaves that one as it is. SysMouseWheelDir, SysMouseDoubleClick, SysMouseAccel, SysMouseMickeys and SysMouseWheelStep read the setup in effect. MOUSE_WHEEL_STEP_MIN and MOUSE_WHEEL_STEP_MAX are the limits of the wheel step, and MOUSE_WHEEL_STEP_DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS and MOUSE_SPEED_DEFAULT are DVX's defaults (what a Default button puts back).
    +Text: SysSetTextSmoothing True smooths the edges of text and SysSetTextSmoothing False stops it; SysTextSmoothing() tells which the user chose. Text is smoothed only when the screen shows more than 256 colors, and only in the fonts and sizes that come smoothed. Every window is drawn again when it changes.
     
    -The pointer and the keyboard: SysMouseX() and SysMouseY() are where the mouse pointer is on the screen, and SysMouseButtons() the buttons held (vbLeftButton, vbRightButton and vbMiddleButton bits) -- also while the pointer is over another program's window, where your MouseMove events do not see it. SysSetMousePos x, y moves the pointer (it is kept on the screen). SysShiftState() is the Shift, Ctrl and Alt keys held now (vbShiftMask, vbCtrlMask and vbAltMask bits). SysSendKey(keyCode, shift) types a key into the window that has the focus as if the user pressed it, like SendKeys in other BASICs: keyCode as a KeyDown event gets it -- a character code such as ASC("a") or 13 for Enter, or a vbKey constant such as vbKeyF1 -- and shift the vbShiftMask, vbCtrlMask and vbAltMask bits held with it (SysSendKey ASC("s"), vbCtrlMask is Ctrl+S). The keys arrive in order, one at a time, once your code has returned to DVX; up to 64 can wait, and SysSendKey returns False when that many are waiting. A program cannot press the mouse buttons: to "click" a control, call its event procedure or set its value.
    +Mouse: SysSetMouse wheelDir, dblClickMs, accel, mickeys, wheelStep applies a whole mouse setup at once: the scroll wheel's direction (1 or -1, the values of PREFS_CHOICE_WHEEL), the double-click time in milliseconds, the acceleration (a value of PREFS_CHOICE_ACCEL), how far the mouse moves for 8 pixels of pointer movement (8 is normal, higher is slower), and the lines scrolled per wheel notch (1 to 10). An accel or mickeys of 0 leaves that one as it is. SysMouseWheelDir, SysMouseDoubleClick, SysMouseAccel, SysMouseMickeys, and SysMouseWheelStep read the setup in effect. MOUSE_WHEEL_STEP_MIN and MOUSE_WHEEL_STEP_MAX are the limits of the wheel step, and MOUSE_WHEEL_STEP_DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS, and MOUSE_SPEED_DEFAULT are DVX's defaults (what a Default button puts back).
    +
    +The pointer and the keyboard: SysMouseX() and SysMouseY() are where the mouse pointer is on the screen, and SysMouseButtons() the buttons held (vbLeftButton, vbRightButton, and vbMiddleButton bits) -- also while the pointer is over another program's window, where your MouseMove events do not see it. SysSetMousePos x, y moves the pointer (it is kept on the screen). SysShiftState() is the Shift, Ctrl, and Alt keys held now (vbShiftMask, vbCtrlMask, and vbAltMask bits). SysSendKey(keyCode, shift) types a key into the window that has the focus as if the user pressed it, like SendKeys in other BASICs: keyCode as a KeyDown event gets it -- a character code such as ASC("a") or 13 for Enter, or a vbKey constant such as vbKeyF1 -- and shift the vbShiftMask, vbCtrlMask, and vbAltMask bits held with it (SysSendKey ASC("s"), vbCtrlMask is Ctrl+S). The keys arrive in order, one at a time, once your code has returned to DVX; up to 64 can wait, and SysSendKey returns False when that many are waiting. A program cannot press the mouse buttons: to "click" a control, call its event procedure or set its value.
     
     Memory: SysMemoryTotalKb() is the computer's memory in KB and SysMemoryFreeKb() how much of it is free (-1 when it cannot be told).
     
     System font: SysFontWidth() and SysFontHeight() are the size of every character of the system font in pixels, and SysFontFirstChar() to SysFontFirstChar() + SysFontCharCount() - 1 the character codes it has. SysFontGlyphRow(code, row) is one row of a character's picture: a bit per pixel, &H80 the leftmost -- for drawing text dot by dot, enlarged or in a font editor of your own.
     
    -Saving: SysColorsStore, SysWallpaperStore and SysMouseStore write the colors, the wallpaper and the mouse setup into a prefs.bas handle, in the sections DVX reads when it starts; PrefsSetVideoMode does the same for a screen mode. Store them in PrefsLoad(PrefsDvxIni$()) and save it, and DVX starts with them next time. SysColorsLoad, SysWallpaperLoad and SysMouseLoad apply what a handle holds. A PrefsCreate handle filled with the Store calls is a snapshot in memory: load it again to undo every change, the way Cancel works in Control Panel.
    +Saving: SysColorsStore, SysWallpaperStore, SysFontsStore, and SysMouseStore write the colors, the wallpaper, text smoothing, and the mouse setup into a prefs.bas handle, in the sections DVX reads when it starts; PrefsSetVideoMode does the same for a screen mode. Store them in PrefsLoad(PrefsDvxIni$()) and save it, and DVX starts with them next time. SysColorsLoad, SysWallpaperLoad, SysFontsLoad, and SysMouseLoad apply what a handle holds. A PrefsCreate handle filled with the Store calls is a snapshot in memory: load it again to undo every change, the way Cancel works in Control Panel.
     
     .code
     Dim snap As Long
    @@ -5437,6 +5479,7 @@ Sub Form1_Load ()
         snap = PrefsCreate()
         SysColorsStore snap
         SysWallpaperStore snap
    +    SysFontsStore snap
         SysMouseStore snap
         oldW = Screen.Width
         oldH = Screen.Height
    @@ -5448,6 +5491,7 @@ Sub cmdOK_Click ()
         ini = PrefsLoad(PrefsDvxIni$())
         SysColorsStore ini
         SysWallpaperStore ini
    +    SysFontsStore ini
         SysMouseStore ini
         PrefsSetVideoMode ini, Screen.Width, Screen.Height, Screen.BitsPerPixel
         If Not PrefsSave(ini) Then MsgBox "The settings could not be saved."
    @@ -5458,6 +5502,7 @@ End Sub
     Sub cmdCancel_Click ()
         SysColorsLoad snap
         SysWallpaperLoad snap
    +    SysFontsLoad snap
         SysMouseLoad snap
         If Screen.Width <> oldW Or Screen.Height <> oldH Or Screen.BitsPerPixel <> oldBpp Then
             SysSetVideoMode oldW, oldH, oldBpp
    @@ -5466,9 +5511,66 @@ Sub cmdCancel_Click ()
     End Sub
     .endcode
     
    +.h2 Fonts
    +
    +Besides the System font (the 8x16 font DVX draws everything with unless told otherwise), DVX comes with font families in its FONTS folder: families for text (sans-serif, serif, and fixed-pitch), display and handwriting families for headings, and pixel fonts. Which of them a computer has depends on how DVX was installed: Setup puts in none, the base set (DejaVu Sans, Sans Mono, and Serif), or all of them. Text in a family that is not installed is drawn in the System font, so a program that cares can look for the family first (FontFamilyCount and FontFamilyName$). With font.bas in the project a program can list them and measure text in them.
    +
    +A family comes in up to four styles -- regular, bold, italic, and bold italic -- and in the pixel sizes it was made at. A font is always drawn at one of those sizes, never scaled: asking for another size gives the nearest one (FontNearestSize says which), and asking for a style a family has no face for makes it from the nearest face, bold by thickening and italic by slanting. Name a font by its family, size, and style on every call; a family name DVX does not have means the System font. Style is 0 for regular or FONT_BOLD + FONT_ITALIC; FONT_UNDERLINE and FONT_STRIKE are lines drawn with the text.
    +
    +.code
    +' With font.bas added to the project: every family and its sizes
    +Dim i As Long
    +Dim j As Long
    +Dim sizes As String
    +
    +For i = 0 To FontFamilyCount() - 1
    +    sizes = ""
    +    For j = 0 To FontSizeCount(FontFamilyName$(i), 0) - 1
    +        sizes = sizes & " " & Str$(FontSizeAt(FontFamilyName$(i), 0, j))
    +    Next j
    +    Print FontFamilyName$(i); ":"; sizes
    +Next i
    +
    +' How wide a heading is in 24-pixel bold DejaVu Sans
    +Print FontTextWidth("DejaVu Sans", 24, FONT_BOLD, "Chapter One")
    +.endcode
    +
    +FontFamilyFixed tells a fixed-pitch family (every character as wide as every other, for columns and code) from a proportional one. FontLineHeight is the height of one line of text and FontAscent the distance from the top of a line to the baseline the letters sit on. FontRescan reads the FONTS folder again after fonts were added or removed. FontLicence$(family, size, style) gives the notice and licence that go with a font: where it came from, its copyright notices, and its licence text (empty for the System font).
    +
    +A program can carry fonts of its own, so its text looks the same whatever fonts were installed. List the .DFN files in the project's Fonts (Project > Properties in the IDE): Make builds them into the program, and they are its fonts whenever it runs, from the IDE too. FontAddFile(path), FontAddData(label, bytes), and FontAddResources(file) add more while it runs. A program's fonts are its own -- other programs never see them -- and they go when it ends. Where a font it carries is of a family that is installed too, its sizes and styles come first and the installed family fills in the rest: a program that carries DejaVu Sans Bold at 36 pixels draws that size from its own font and every other size and style of DejaVu Sans from the FONTS folder. Fonts are made from TrueType, Type 1, and bitmap fonts with the SDK's mkfont, or copied from the FONTS folder; each .DFN carries its own licence.
    +
    +.code
    +' With font.bas added to the project: a title in a font the program carries
    +If FontAddFile(App.Path + "\TITLE.DFN") Then
    +    lblTitle.FontName = "DejaVu Sans"
    +    lblTitle.FontSize = 36
    +    lblTitle.FontBold = True
    +End If
    +.endcode
    +
    +Every control and every form can have a font of its own, set with the properties FontName, FontSize (in pixels), FontBold, FontItalic, FontUnderline, and FontStrikethru, as in Visual Basic; they need no include file. A control that is given no font uses the font of the control it is in (a Frame, for example), else its form's, so setting a form's font changes every control on it that has none. Reading one of the properties gives the font the object draws with: a FontSize the family does not come in reads back as the nearest size it has, and a FontName DVX does not have reads back as "System". Setting one changes only that part of the font. Controls that edit text in columns (TextBox, TextArea, ComboBox, SpinButton, and Terminal) take only fixed-pitch families and show the System font in place of a proportional one. Print, TextWidth, TextHeight, PrintAccel, and TextWidthAccel on forms and picture boxes use the object's font. In the form designer, the Font row of the Properties window sets the six properties at once, and they are saved in the .frm file; DlgFont (see Font Dialog) lets the user choose a font while the program runs.
    +
    +.code
    +Sub Form1_Load ()
    +    Me.FontName = "DejaVu Sans"        ' every control without a font of its own
    +    Me.FontSize = 14
    +    lblTitle.FontSize = 24              ' the title larger, in DejaVu Sans too
    +    lblTitle.FontBold = True
    +    txtNotes.FontName = "DejaVu Sans Mono"
    +End Sub
    +.endcode
    +
    +Two controls mix fonts in one control. A RichLabel shows text built by calls -- SetFont, SetStyle, and SetColor choose how the text added next looks, AddText adds it -- in paragraphs aligned left, centered, or right. A RichEdit is a text box the user writes such text in, like a small word processor: the Sel properties (SelBold, SelFontName, SelColor, SelAlignment, ...) read and change the selection's look, TextRTF holds the whole text with its looks as RTF, and LoadFile and SaveFile read and write files. richtext.bas turns RTF into plain text and back without a control. See the controls' own pages for their properties.
    +
    +.code
    +Sub cmdBold_Click ()
    +    RichEdit1.SelBold = Not RichEdit1.SelBold
    +End Sub
    +.endcode
    +
     .h2 Held Keys for Games
     
    -With game.bas in the project, GameKeyDown(keyCode) tells whether a key is being held down right now, so a game can move something for as long as a key is held instead of waiting for key presses. It returns True only while one of the program's own windows is active. Name the key with one of the GAMEKEY_ constants (the arrow keys, Shift, Ctrl, Alt, Space, Enter, Esc and a few letters) or with its PC keyboard scan code (add &H80 for the grey keys between the typing keys and the number pad). A Timer polls it:
    +With game.bas in the project, GameKeyDown(keyCode) tells whether a key is being held down right now, so a game can move something for as long as a key is held instead of waiting for key presses. It returns True only while one of the program's own windows is active. Name the key with one of the GAMEKEY_ constants (the arrow keys, Shift, Ctrl, Alt, Space, Enter, Esc, and a few letters) or with its PC keyboard scan code (add &H80 for the grey keys between the typing keys and the number pad). A Timer polls it:
     
     .code
     Sub Timer1_Timer ()
    @@ -5493,7 +5595,7 @@ Loop Until quitting
     
     .h2 Joysticks
     
    -With joystick.bas in the project, a program reads joysticks and game pads. JoyCount() says how many there are; they are numbered from 1, and so are their axes, buttons and hats. JoyX(joy) and JoyY(joy) say where the stick points, from -32768 (all the way left or up) to 32767 (all the way right or down), 0 in the middle; JoyAxis(joy, axis) reads any axis (3 and 4 are a throttle or rudder). JoyButton(joy, button) is True while the button is held down, and also once after a quick press that happened since the program last asked about that button, so a Timer never misses a shot. JoyHat(joy, hat) gives the direction a hat switch is pushed in degrees (0 is up, 90 right) or JOY_HAT_CENTERED. JoyName$(joy), JoyAxes(joy) and JoyButtons(joy) describe the joystick.
    +With joystick.bas in the project, a program reads joysticks and game pads. JoyCount() says how many there are; they are numbered from 1, and so are their axes, buttons, and hats. JoyX(joy) and JoyY(joy) say where the stick points, from -32768 (all the way left or up) to 32767 (all the way right or down), 0 in the middle; JoyAxis(joy, axis) reads any axis (3 and 4 are a throttle or rudder). JoyButton(joy, button) is True while the button is held down, and also once after a quick press that happened since the program last asked about that button, so a Timer never misses a shot. JoyHat(joy, hat) gives the direction a hat switch is pushed in degrees (0 is up, 90 right) or JOY_HAT_CENTERED. JoyName$(joy), JoyAxes(joy), and JoyButtons(joy) describe the joystick.
     
     .code
     Sub Timer1_Timer ()
    @@ -5504,9 +5606,9 @@ Sub Timer1_Timer ()
     End Sub
     .endcode
     
    -When no joystick is connected the keyboard stands in for one (the arrow keys, with Left Ctrl, Space, Left Shift and Z as buttons), and it only answers while one of the program's windows is active. Joysticks are calibrated, and the keyboard joystick's keys chosen, on the Control Panel's Game Controllers tab. QBasic's STICK and STRIG work too, without joystick.bas.
    +When no joystick is connected the keyboard stands in for one (the arrow keys, with Left Ctrl, Space, Left Shift, and Z as buttons), and it only answers while one of the program's windows is active. Joysticks are calibrated, and the keyboard joystick's keys chosen, on the Control Panel's Game Controllers tab. QBasic's STICK and STRIG work too, without joystick.bas.
     
    -joystick.bas can also set joysticks up, as the Control Panel does. JoyHats(joy) counts the hats, JoyAxisPresent(joy, axis) is True when an axis answers, JoyConnected(joy) is False once a joystick stops answering, and JoyCanCalibrate(joy) is True for one with analog axes. For calibrating, JoyRawAxis(joy, axis) gives an axis's raw reading and JoyRawButtons(joy) the buttons held (1 for button 1, 2 for button 2, 4 for button 3, ... added up); JoySetCalibration(joy, axis, minV, centerV, maxV) takes the readings at one end, the middle and the other end, and JoyClearCalibration(joy, axis) lets the axis find its own range again. JoyDeadZone(joy) and JoySetDeadZone(joy, percent) are the dead zone round the middle (0 to 50 percent). JoyChoiceCount, JoyChoiceLabel$, JoyChoice and JoySetChoice list and change the game port's mode (JOY_CHOICE_GAMEPORT) and when the keyboard joystick stands in (JOY_CHOICE_FALLBACK); JoyKeyCount and JoyKeyName$(index) list the keys the keyboard joystick can use, and JoyKbdSlot(slot) and JoySetKbdSlot(slot, keyIndex) its keys for up, down, left, right and buttons 1 to 4 (slots 0 to 7). These lists are numbered from 0, so they fill a DropDown in order. JoyKeyIndex(keyName) gives a key's number by its name (-1 when unknown), JoyKeyCode(index) its raw key code, which GameKeyDown in game.bas takes, and JoyKeyNameOf$(rawCode) the name of a raw key code. JoyIsKeyboard(joy) is True for the keyboard standing in for a joystick, and JoyCalibration(joy, axis, part) reads back an axis's calibration: part JOY_CAL_MIN, JOY_CAL_CENTER or JOY_CAL_MAX of the readings set with JoySetCalibration, or of the range the axis found by itself (0 for a joystick that cannot be calibrated). JoySetting$(key, default) and JoySetSetting key, value reach any other joystick setting; JOY_KEY_POLL_HZ is the key of how often joysticks are read, JOY_POLL_HZ_MIN to JOY_POLL_HZ_MAX times a second (JOY_POLL_HZ_DEFAULT normally), and dead zones go up to JOY_DEADZONE_MAX percent (JOY_DEADZONE_DEFAULT normally). After a new game port mode, JoyRescan() looks for joysticks again; it returns the new count, or -1 while another program is using a joystick. Every change applies at once; JoySettingsStore handle puts them all into a settings handle from prefs.bas for the program to save, and JoySettingsLoad handle takes them back from one, to undo.
    +joystick.bas can also set joysticks up, as the Control Panel does. JoyHats(joy) counts the hats, JoyAxisPresent(joy, axis) is True when an axis answers, JoyConnected(joy) is False once a joystick stops answering, and JoyCanCalibrate(joy) is True for one with analog axes. For calibrating, JoyRawAxis(joy, axis) gives an axis's raw reading and JoyRawButtons(joy) the buttons held (1 for button 1, 2 for button 2, 4 for button 3, ... added up); JoySetCalibration(joy, axis, minV, centerV, maxV) takes the readings at one end, the middle, and the other end, and JoyClearCalibration(joy, axis) lets the axis find its own range again. JoyDeadZone(joy) and JoySetDeadZone(joy, percent) are the dead zone round the middle (0 to 50 percent). JoyChoiceCount, JoyChoiceLabel$, JoyChoice, and JoySetChoice list and change the game port's mode (JOY_CHOICE_GAMEPORT) and when the keyboard joystick stands in (JOY_CHOICE_FALLBACK); JoyKeyCount and JoyKeyName$(index) list the keys the keyboard joystick can use, and JoyKbdSlot(slot) and JoySetKbdSlot(slot, keyIndex) its keys for up, down, left, right, and buttons 1 to 4 (slots 0 to 7). These lists are numbered from 0, so they fill a DropDown in order. JoyKeyIndex(keyName) gives a key's number by its name (-1 when unknown), JoyKeyCode(index) its raw key code, which GameKeyDown in game.bas takes, and JoyKeyNameOf$(rawCode) the name of a raw key code. JoyIsKeyboard(joy) is True for the keyboard standing in for a joystick, and JoyCalibration(joy, axis, part) reads back an axis's calibration: part JOY_CAL_MIN, JOY_CAL_CENTER, or JOY_CAL_MAX of the readings set with JoySetCalibration, or of the range the axis found by itself (0 for a joystick that cannot be calibrated). JoySetting$(key, default) and JoySetSetting key, value reach any other joystick setting; JOY_KEY_POLL_HZ is the key of how often joysticks are read, JOY_POLL_HZ_MIN to JOY_POLL_HZ_MAX times a second (JOY_POLL_HZ_DEFAULT normally), and dead zones go up to JOY_DEADZONE_MAX percent (JOY_DEADZONE_DEFAULT normally). After a new game port mode, JoyRescan() looks for joysticks again; it returns the new count, or -1 while another program is using a joystick. Every change applies at once; JoySettingsStore handle puts them all into a settings handle from prefs.bas for the program to save, and JoySettingsLoad handle takes them back from one, to undo.
     
     .code
     Sub cmdCenter_Click ()
    @@ -5525,7 +5627,7 @@ End Sub
     
     .h2 Sound
     
    -With sound.bas in the project, a program plays sound files and tones through whatever sound hardware the computer has: a Sound Blaster, the PC speaker (tones only, unless sampled sound on the speaker is switched on) or nothing at all, in which case the program works the same but is silent. Several programs can play at once, and everything a program plays stops when it ends.
    +With sound.bas in the project, a program plays sound files and tones through whatever sound hardware the computer has: a Sound Blaster, the PC speaker (tones only, unless sampled sound on the speaker is switched on), or nothing at all, in which case the program works the same but is silent. Several programs can play at once, and everything a program plays stops when it ends.
     
     PlayWave(fileName) plays a WAV or VOC file once and PlayWaveLoop(fileName) over and over; PlayTone(frequency, milliseconds, volume) plays a tone of frequency hertz (20 to 20000) and PlayNoise(frequency, milliseconds, volume) noise, which sounds brighter the higher the frequency; a length of 0 plays until stopped. Each returns a voice number, or SOUND_FAILED (0) when the sound could not be played, and then SoundError$() says why. StopWave(voice) stops one sound, StopAllSounds() all of the program's sounds, and SoundPlaying(voice) is True while a sound is still playing. SoundVolume(volume) sets the volume of everything the program plays. Volumes are percent (0 to 100). SoundInfo$() describes the sound device, like "Sound Blaster 16 (DSP 4.05) at 220h, 22050 Hz, 16-bit stereo".
     
    @@ -5541,11 +5643,11 @@ Sub cmdFire_Click ()
     End Sub
     .endcode
     
    -WAV files must be uncompressed (8 or 16 bits, mono or stereo); VOC files may be 8- or 16-bit. QBasic's BEEP, SOUND and PLAY work too, without sound.bas.
    +WAV files must be uncompressed (8 or 16 bits, mono or stereo); VOC files may be 8- or 16-bit. QBasic's BEEP, SOUND, and PLAY work too, without sound.bas.
     
    -PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator) and "running" are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits" and "channels" are numbers. SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played.
    +PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator), and "running" are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits", and "channels" are numbers. SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played.
     
    -A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. A program's sounds are freed when it ends.
    +A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan, and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. A program's sounds are freed when it ends.
     
     .code
     Dim Shared laser As Long
    @@ -5561,7 +5663,7 @@ Sub Fire (x As Integer)
     End Sub
     .endcode
     
    -A program can also make its sound as it goes, for speech or music of its own. SoundStreamOpen(rate, bits, channels, bufferMs) opens a stream that starts playing at once (silence until data arrives) and holds bufferMs milliseconds of sound (0 for the usual SOUND_STREAM_MS_DEFAULT; SOUND_STREAM_MS_MIN to SOUND_STREAM_MS_MAX). SoundStreamWrite(stream, data) adds raw sound data in the stream's format and returns how many bytes it took; keep the rest and write it again later. SoundStreamSpace(stream) is the room left and SoundStreamQueued(stream) what is waiting to play, both in samples (a stereo sample counts once). SoundStreamVoice(stream) is the stream's voice, for SoundSetVolume, SoundSetPan and SoundSetPitch, and SoundStreamClose stream stops it. A Timer keeps a stream fed:
    +A program can also make its sound as it goes, for speech or music of its own. SoundStreamOpen(rate, bits, channels, bufferMs) opens a stream that starts playing at once (silence until data arrives) and holds bufferMs milliseconds of sound (0 for the usual SOUND_STREAM_MS_DEFAULT; SOUND_STREAM_MS_MIN to SOUND_STREAM_MS_MAX). SoundStreamWrite(stream, data) adds raw sound data in the stream's format and returns how many bytes it took; keep the rest and write it again later. SoundStreamSpace(stream) is the room left and SoundStreamQueued(stream) what is waiting to play, both in samples (a stereo sample counts once). SoundStreamVoice(stream) is the stream's voice, for SoundSetVolume, SoundSetPan, and SoundSetPitch, and SoundStreamClose stream stops it. A Timer keeps a stream fed:
     
     .code
     Sub Timer1_Timer ()
    @@ -5572,9 +5674,9 @@ Sub Timer1_Timer ()
     End Sub
     .endcode
     
    -On a computer with an FM synthesizer chip (AdLib and most Sound Blasters), a program can play instruments of its own. FmInfo$(field) describes the chip: "description" (like "OPL3 FM synthesizer at 388h", "" when there is none), "voices", "free" (voices no program has), "stereo" and "owned" (one program has the whole chip). FmAlloc() takes one of its two-operator voices (0 when none is free), FmWrite(voice, register, value) sets one of the voice's registers (the FM_ constants: FM_MOD_CHAR, FM_CAR_LEVEL, FM_FNUM_LOW, FM_KEY_BLOCK with FM_KEY_ON, ...; True when it worked), and FmFree voice gives it back. A player of chip music files can take the whole chip with FmRawAcquire() (False while another program has it), set any register with FmRawWrite(register, value) (bank * FM_BANK_REGS + register; the second bank only on an OPL3) and give it back with FmRawRelease. A frequency number for FM_FNUM_LOW and FM_KEY_BLOCK is hertz * 2 ^ 20 / (FM_CHIP_HZ * 2 ^ block).
    +On a computer with an FM synthesizer chip (AdLib and most Sound Blasters), a program can play instruments of its own. FmInfo$(field) describes the chip: "description" (like "OPL3 FM synthesizer at 388h", "" when there is none), "voices", "free" (voices no program has), "stereo", and "owned" (one program has the whole chip). FmAlloc() takes one of its two-operator voices (0 when none is free), FmWrite(voice, register, value) sets one of the voice's registers (the FM_ constants: FM_MOD_CHAR, FM_CAR_LEVEL, FM_FNUM_LOW, FM_KEY_BLOCK with FM_KEY_ON, ...; True when it worked), and FmFree voice gives it back. A player of chip music files can take the whole chip with FmRawAcquire() (False while another program has it), set any register with FmRawWrite(register, value) (bank * FM_BANK_REGS + register; the second bank only on an OPL3) and give it back with FmRawRelease. A frequency number for FM_FNUM_LOW and FM_KEY_BLOCK is hertz * 2 ^ 20 / (FM_CHIP_HZ * 2 ^ block).
     
    -sound.bas also changes DVX's own sound settings, as the Control Panel's Sounds page does. DVX plays a sound for each of SoundEventCount() events, numbered from 0: SoundEventLabel$(eventIdx) names one ("Start DVX", "Error", ...) and SoundEventKey$(eventIdx) gives its key in DVX.INI. With a settings handle from prefs.bas (PrefsLoad(PrefsDvxIni$())), SoundEventFile$(handle, eventIdx) is the event's sound file ("" for none) and SoundSetEventFile handle, eventIdx, fileName changes it; after the program saves the file, SoundEventsReload makes DVX play the new sounds. SoundTestFile(fileName) plays a file the way DVX plays its event sounds (0 when it cannot, and SoundError$() says why), and SoundFolder$() is the folder of DVX's own sounds. SoundMasterVolume() and SoundSetMasterVolume percent are the volume of all sound from every program. SoundDriverCount() reads the list of sound drivers and SoundDriverName$(index) names each; SoundDriverAuto$() is the setting that means "find the best one". SoundMidiCount() and SoundMidiLabel$(index) list where MIDI music can play. SoundSettingDriver$(handle), SoundSettingMidi(handle) and SoundSettingVolume(handle) read the sound driver, MIDI choice and master volume from the settings, and SoundSettingsStore handle, driver, midi, volume stores them (a new driver or MIDI choice takes effect when DVX next starts).
    +sound.bas also changes DVX's own sound settings, as the Control Panel's Sounds page does. DVX plays a sound for each of SoundEventCount() events, numbered from 0: SoundEventLabel$(eventIdx) names one ("Start DVX", "Error", ...) and SoundEventKey$(eventIdx) gives its key in DVX.INI. With a settings handle from prefs.bas (PrefsLoad(PrefsDvxIni$())), SoundEventFile$(handle, eventIdx) is the event's sound file ("" for none) and SoundSetEventFile handle, eventIdx, fileName changes it; after the program saves the file, SoundEventsReload makes DVX play the new sounds. SoundTestFile(fileName) plays a file the way DVX plays its event sounds (0 when it cannot, and SoundError$() says why), and SoundFolder$() is the folder of DVX's own sounds. SoundMasterVolume() and SoundSetMasterVolume percent are the volume of all sound from every program. SoundDriverCount() reads the list of sound drivers and SoundDriverName$(index) names each; SoundDriverAuto$() is the setting that means "find the best one". SoundMidiCount() and SoundMidiLabel$(index) list where MIDI music can play. SoundSettingDriver$(handle), SoundSettingMidi(handle), and SoundSettingVolume(handle) read the sound driver, MIDI choice, and master volume from the settings, and SoundSettingsStore handle, driver, midi, volume stores them (a new driver or MIDI choice takes effect when DVX next starts).
     
     .code
     Dim ini As Long
    @@ -5587,11 +5689,11 @@ For i = 0 To SoundEventCount() - 1
     Next i
     .endcode
     
    -.link lang.sound See: BEEP, SOUND and PLAY
    +.link lang.sound See: BEEP, SOUND, and PLAY
     
     .h2 Music
     
    -With music.bas in the project, a program plays music files: tracker modules (MOD, S3M, XM and IT files, made of recorded instrument sounds) wherever the computer can play sampled sound, and MIDI files (.MID, types 0 and 1) on the computer's music synthesizer, or on a built-in synthesizer that sounds much the same when there is none. Several programs can play music at once, and everything a program plays stops when it ends.
    +With music.bas in the project, a program plays music files: tracker modules (MOD, S3M, XM, and IT files, made of recorded instrument sounds) wherever the computer can play sampled sound, and MIDI files (.MID, types 0 and 1) on the computer's music synthesizer, or on a built-in synthesizer that sounds much the same when there is none. Several programs can play music at once, and everything a program plays stops when it ends.
     
     MusicLoad(fileName) loads a music file and returns its music number, or MUSIC_FAILED (0) when it could not be loaded; MusicError$() then says why. MusicPlay(music, loopIt) plays it from where it is (the start after loading or stopping); with loopIt True it starts over at the end until it is stopped. MusicStop(music) stops and goes back to the start, MusicPause(music) and MusicResume(music) pause and go on, and MusicPlaying(music) is True while it plays. MusicVolume(music, volume) sets its volume in percent (0 to 100). MusicPosition(music) and MusicLength(music) give the place and the length in milliseconds, and MusicSeek(music, milliseconds) goes to a place (a module goes to the start of the part that holds it). MusicFree(music) stops a music and forgets it.
     
    @@ -5609,7 +5711,7 @@ Sub Form1_Load ()
     End Sub
     .endcode
     
    -A program can also play notes itself, like a keyboard. MusicMidiOpen() returns a live music that is already playing. MusicNoteOn(music, channel, note, velocity) starts a note and MusicNoteOff(music, channel, note) ends it; MusicProgram(music, channel, program) chooses a channel's instrument, MusicControl(music, channel, controller, value) sets a controller (7 volume, 10 pan, 11 expression, 64 the sustain pedal, 123 all notes off) and MusicPitchBend(music, channel, bend) bends the channel's notes (-8192 to 8191, two semitones either way at the ends). Channels are 1 to 16, and channel 10 (MUSIC_DRUMS) plays drums, one per note. Notes, velocities, instruments and controller values are 0 to 127; middle C is note 60, and instruments are numbered as in General MIDI (0 piano, 24 guitar, 40 violin, 56 trumpet, 73 flute). The same calls work on a MIDI file that is playing.
    +A program can also play notes itself, like a keyboard. MusicMidiOpen() returns a live music that is already playing. MusicNoteOn(music, channel, note, velocity) starts a note and MusicNoteOff(music, channel, note) ends it; MusicProgram(music, channel, program) chooses a channel's instrument, MusicControl(music, channel, controller, value) sets a controller (7 volume, 10 pan, 11 expression, 64 the sustain pedal, 123 all notes off), and MusicPitchBend(music, channel, bend) bends the channel's notes (-8192 to 8191, two semitones either way at the ends). Channels are 1 to 16, and channel 10 (MUSIC_DRUMS) plays drums, one per note. Notes, velocities, instruments, and controller values are 0 to 127; middle C is note 60, and instruments are numbered as in General MIDI (0 piano, 24 guitar, 40 violin, 56 trumpet, 73 flute). The same calls work on a MIDI file that is playing.
     
     .code
     Dim Shared keys As Long
    @@ -5630,9 +5732,9 @@ Sub tmrNoteOff_Timer ()
     End Sub
     .endcode
     
    -MusicGetVolume(music) gives a music's volume in percent. MusicInfo$(music, field) tells about it, by name in any case: "title" (a module's own title), "output" (where it plays, for people), "kind" ("module", "midi" or "live"), "where" ("stream", "mpu401", "fm" or "software"; "" before it first plays), and "paused" and "looping" (True or False as text; VAL gives the value).
    +MusicGetVolume(music) gives a music's volume in percent. MusicInfo$(music, field) tells about it, by name in any case: "title" (a module's own title), "output" (where it plays, for people), "kind" ("module", "midi", or "live"), "where" ("stream", "mpu401", "fm", or "software"; "" before it first plays), and "paused" and "looping" (True or False as text; VAL gives the value).
     
    -A program that sends MIDI itself (system exclusive messages, a sequencer of its own) can have the MIDI port, one program at a time, when the settings send MIDI music to the MIDI port. MidiPortInfo$(field) describes it: "description" ("" when there is none), "present" and "owned" (a program has it). MidiAcquire() takes the port (False when another program has it or there is none), MidiPut(byteValue) sends one byte (False when the port could not take it yet: send it again) and MidiRelease gives the port back.
    +A program that sends MIDI itself (system exclusive messages, a sequencer of its own) can have the MIDI port, one program at a time, when the settings send MIDI music to the MIDI port. MidiPortInfo$(field) describes it: "description" ("" when there is none), "present", and "owned" (a program has it). MidiAcquire() takes the port (False when another program has it or there is none), MidiPut(byteValue) sends one byte (False when the port could not take it yet: send it again), and MidiRelease gives the port back.
     
     .h2 Network Connections
     
    @@ -5745,7 +5847,7 @@ End If
     
     .h2 Connections and Terminals
     
    -With conn.bas in the project, a program talks to another computer the way the Terminal program does, whatever is in between: a serial port, a modem (it sends its setup command, dials, dials again when the line is busy and notices when the carrier is lost), the secure link to the DVX host proxy (encrypted, and authenticated with a passphrase), or Telnet over the network. ConnCreate makes a connection (a number); ConnSetSerial, ConnSetModem, ConnSetHost and ConnSetPassphrase set it up, and ConnOpen(conn, type) starts it. Opening only starts: dialing and the key exchange go on while the program runs, so call ConnPoll often from a Timer. ConnState says where it is (CONN_CLOSED, CONN_OPENING, CONN_OPEN) and ConnStatus$ what is happening or why it ended; ConnStatusSerial changes whenever ConnStatus$ does.
    +With conn.bas in the project, a program talks to another computer the way the Terminal program does, whatever is in between: a serial port, a modem (it sends its setup command, dials, dials again when the line is busy, and notices when the carrier is lost), the secure link to the DVX host proxy (encrypted, and authenticated with a passphrase), or Telnet over the network. ConnCreate makes a connection (a number); ConnSetSerial, ConnSetModem, ConnSetHost, and ConnSetPassphrase set it up, and ConnOpen(conn, type) starts it. Opening only starts: dialing and the key exchange go on while the program runs, so call ConnPoll often from a Timer. ConnState says where it is (CONN_CLOSED, CONN_OPENING, CONN_OPEN) and ConnStatus$ what is happening or why it ended; ConnStatusSerial changes whenever ConnStatus$ does.
     
     ConnAttach conn, "Term1" puts what arrives on the Terminal control Term1 (ConnPoll moves it there) and sends what is typed in it. ConnPoll then also appends what arrives to a capture file (ConnCapture), answers the other side's ENQ with an answerback (ConnSetAnswerback), and notices a ZMODEM sender starting a download: ConnZmodemRequest is True once, and the program answers with ConnReceiveFiles in the same Timer event. ConnSetEcho shows what is typed on the Terminal too. Without a Terminal, read with ConnRead$ and send with ConnWrite (any bytes, CHR$(0) too). ConnOpen returns CONN_OK or a CONN_ERR_ code; ConnErrorText$ puts the code in words. ConnSetXferWindow conn, bytes limits how much a ZMODEM sender may send before it waits for an answer (0 = no limit, up to XFER_WINDOW_MAX; -1, the default, picks by the line). ConnSendBreak holds a serial line in the break state for a moment; ConnClose hangs up and ConnFree forgets the connection.
     
    @@ -5768,7 +5870,7 @@ End Sub
     
     .h2 File Transfers
     
    -With xfer.bas in the project, a program sends and receives files with XMODEM, XMODEM-1K, YMODEM or ZMODEM. A transfer is a number: XferSendFile and XferReceive start one on a serial port opened with SerOpen (comm.bas), and ConnSendFiles and ConnReceiveFiles start one on a connection (conn.bas), with the options Terminal has: escaping every control character, asking the receiver to continue interrupted files, and what to do with a file that is already there. On a serial port XferPoll moves the transfer along; on a connection ConnPoll does. XferStatus tells where it is (XFER_RUNNING until it ends with XFER_DONE, XFER_FAILED or XFER_CANCELLED), and XferBytes, XferSize, XferFile$, XferFileIndex, XferFileCount, XferBatchBytes, XferBatchSize, XferResumedAt and XferErrors show its progress; XferMessage$ says how it ended. A receive with XFER_EXIST_ASK stops in XFER_ASKING when a file already exists: XferAskSize, XferAskRemoteSize (the size the sender offers, -1 when it did not say) and XferCanResume describe it, and XferAnswer continues with an XFER_EXIST_ choice. XferCancel stops a transfer and XferClose frees it.
    +With xfer.bas in the project, a program sends and receives files with XMODEM, XMODEM-1K, YMODEM, or ZMODEM. A transfer is a number: XferSendFile and XferReceive start one on a serial port opened with SerOpen (comm.bas), and ConnSendFiles and ConnReceiveFiles start one on a connection (conn.bas), with the options Terminal has: escaping every control character, asking the receiver to continue interrupted files, and what to do with a file that is already there. On a serial port XferPoll moves the transfer along; on a connection ConnPoll does. XferStatus tells where it is (XFER_RUNNING until it ends with XFER_DONE, XFER_FAILED, or XFER_CANCELLED), and XferBytes, XferSize, XferFile$, XferFileIndex, XferFileCount, XferBatchBytes, XferBatchSize, XferResumedAt, and XferErrors show its progress; XferMessage$ says how it ended. A receive with XFER_EXIST_ASK stops in XFER_ASKING when a file already exists: XferAskSize, XferAskRemoteSize (the size the sender offers, -1 when it did not say), and XferCanResume describe it, and XferAnswer continues with an XFER_EXIST_ choice. XferCancel stops a transfer and XferClose frees it.
     
     XferCrc16(bytes) and XferCrc32(bytes) give the check values the protocols use (CRC-16 as XMODEM computes it, 0 to 65535; the CRC-32 of ZMODEM and ZIP files as a Long, negative when its top bit is set) for any bytes. XferSafeName$(name, folder) gives the name a receive into folder stores a sender's file name as: no folder or device name; a long name, its case kept, where folder's disk takes long names (a character a name cannot hold becomes _), else an 8.3 name of characters every disk accepts.
     
    @@ -5781,7 +5883,7 @@ With listhelp.bas in the project, a list or icon view a program draws itself --
       --------                                            -----------
       ListSelClick sel, index, shift                      A click on an item, with the MouseDown event's Shift: alone it selects only that item, with Ctrl it turns the item on or off, with Shift it selects the items from the last click up to it.
       ListSelKey(sel, keyCode, shift)                     Ctrl+A selects every item (returns LISTSEL_KEY_ALL), Space turns the current item on or off (LISTSEL_KEY_TOGGLE); LISTSEL_KEY_NONE for other keys.
    -  ListSelNavigate(sel, keyCode, shift, columns, pageRows)  The arrow keys, Home, End, PgUp and PgDn, in a list (columns 1) or a grid with columns items per row, pageRows rows a page; with Shift they select from the last click. The new current item, or -1 when nothing moved.
    +  ListSelNavigate(sel, keyCode, shift, columns, pageRows)  The arrow keys, Home, End, PgUp, and PgDn, in a list (columns 1) or a grid with columns items per row, pageRows rows a page; with Shift they select from the last click. The new current item, or -1 when nothing moved.
       ListSelMove sel, index, shift                       Make an item current as the keyboard does (after type-ahead).
       ListSelCursor(sel), ListSelSetCursor sel, index     The current item (-1: none); make one item current and the only one selected.
       ListSelIsSelected(sel, index), ListSelSelectedCount(sel)  Whether an item is selected; how many are.
    @@ -5813,15 +5915,15 @@ End Sub
     
     Selections a program leaves open are freed when it ends.
     
    -An icon view drawn with listhelp.bas looks and behaves like Program Manager's: the same cells for large icons, small icons and the list, the same frame and scroll bar, a large icon's name on two lines, and the same rules for the keys, dragging and dropping. The grid functions take the view mode (LISTICON_VIEW_LARGE, LISTICON_VIEW_SMALL or LISTICON_VIEW_LIST), the size of a character (TextWidth("W") and TextHeight("W")), the size of the whole view, how many items it shows, and rowsTaken: LISTICON_BY_ORDER when the items fill the grid in order, else how many rows items placed in cells of their own take. Draw the view's frame with DrawBevel (LISTICON_FRAME wide; with the face -1 after the items, so a row cut off at the bottom stops at the frame) and its scroll bar with DrawScrollBar as a control's bar (its control argument True); the grid leaves room for it.
    +An icon view drawn with listhelp.bas looks and behaves like Program Manager's: the same cells for large icons, small icons, and the list, the same frame and scroll bar, a large icon's name on two lines, and the same rules for the keys, dragging and dropping. The grid functions take the view mode (LISTICON_VIEW_LARGE, LISTICON_VIEW_SMALL, or LISTICON_VIEW_LIST), the size of a character (TextWidth("W") and TextHeight("W")), the size of the whole view, how many items it shows, and rowsTaken: LISTICON_BY_ORDER when the items fill the grid in order, else how many rows items placed in cells of their own take. Draw the view's frame with DrawBevel (LISTICON_FRAME wide; with the face -1 after the items, so a row cut off at the bottom stops at the frame) and its scroll bar with DrawScrollBar as a control's bar (its control argument True); the grid leaves room for it.
     
     .table
       Function                                            Description
       --------                                            -----------
    -  ListIconMetric(mode, metric, cw, ch, w, h, count, rowsTaken)  One value of the grid: a cell's size (LISTICON_CELL_W, LISTICON_CELL_H), where its picture goes and how big it is (LISTICON_PICTURE_X, LISTICON_PICTURE_Y, LISTICON_PICTURE_SIZE), where its name goes and how many characters it shows (LISTICON_TEXT_X, LISTICON_TEXT_Y, LISTICON_NAME_CHARS; a large icon's two lines are each centred in the cell), the list's detail column (LISTICON_DETAIL_X, 0 in the other views); the inside of the frame less the scroll bar (LISTICON_INNER_X, LISTICON_INNER_Y, LISTICON_INNER_W, LISTICON_INNER_H), the columns, the rows shown whole and in all (LISTICON_COLUMNS, LISTICON_ROWS_SHOWN, LISTICON_ROWS), the largest top row (LISTICON_TOP_ROW_MAX), True when the scroll bar is needed (LISTICON_SCROLLBAR), where the text of an empty view goes (LISTICON_EMPTY_X, LISTICON_EMPTY_Y), the frame's width (LISTICON_FRAME) and the rows a wheel notch scrolls (LISTICON_WHEEL_ROWS).
    +  ListIconMetric(mode, metric, cw, ch, w, h, count, rowsTaken)  One value of the grid: a cell's size (LISTICON_CELL_W, LISTICON_CELL_H), where its picture goes and how big it is (LISTICON_PICTURE_X, LISTICON_PICTURE_Y, LISTICON_PICTURE_SIZE), where its name goes and how many characters it shows (LISTICON_TEXT_X, LISTICON_TEXT_Y, LISTICON_NAME_CHARS; a large icon's two lines are each centred in the cell), the list's detail column (LISTICON_DETAIL_X, 0 in the other views); the inside of the frame less the scroll bar (LISTICON_INNER_X, LISTICON_INNER_Y, LISTICON_INNER_W, LISTICON_INNER_H), the columns, the rows shown whole and in all (LISTICON_COLUMNS, LISTICON_ROWS_SHOWN, LISTICON_ROWS), the largest top row (LISTICON_TOP_ROW_MAX), True when the scroll bar is needed (LISTICON_SCROLLBAR), where the text of an empty view goes (LISTICON_EMPTY_X, LISTICON_EMPTY_Y), the frame's width (LISTICON_FRAME), and the rows a wheel notch scrolls (LISTICON_WHEEL_ROWS).
       ListIconCellAt(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y)  The cell under x, y with the view scrolled to topRow, numbered row * columns + column; -1 outside the grid.
       ListIconLabel$(name, maxChars, lineNo)              Line 1 or 2 of a large icon's name, maxChars characters a line (LISTICON_NAME_CHARS): the first line ends at the last blank that fits, else it is cut; the second is the rest, cut. "" for a line the name does not need.
    -  ListIconNavigate(keyCode, current, cells, columns, pageRows)  The item the arrow keys, Home, End, PgUp or PgDn move to when the items stand in cells of their own, gaps allowed: cells holds each item's column and row, all separated by commas ("0,0,2,0,1,1"). -1 for another key or when no item is that way.
    +  ListIconNavigate(keyCode, current, cells, columns, pageRows)  The item the arrow keys, Home, End, PgUp, or PgDn move to when the items stand in cells of their own, gaps allowed: cells holds each item's column and row, all separated by commas ("0,0,2,0,1,1"). -1 for another key or when no item is that way.
       ListIconScrollPress(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y)  A left press on the scroll bar: the top row after it (an arrow moves a row, the trough a page, the thumb nothing); -1 when x, y is not on the scroll bar.
       ListIconScrollGrab(mode, cw, ch, w, h, count, rowsTaken, topRow, x, y)  How far below the top of the thumb a press is; -1 when it is not on the thumb.
       ListIconScrollDrag(mode, cw, ch, w, h, count, rowsTaken, grab, y)  The top row with the thumb, held grab pixels below its top, dragged to y. While the button is down the control gets the mouse even outside it.
    @@ -5857,7 +5959,7 @@ With security.bas in the project, a program has the building blocks of DVX's enc
       SecCipherCreate(key)                                A cipher with a SEC_XTEA_KEY_SIZE-byte key; 0 for a key of another length.
       SecCipherCrypt$(cipher, bytes)                      Encrypt -- or decrypt, the same call -- bytes. SecCipherSetCounter cipher, n sets the position in the key stream; the same key and position must never encrypt two different messages. SecCipherFree cipher frees it.
       SecSha256$(bytes)                                   The SHA-256 hash (SEC_SHA256_SIZE bytes). For data in pieces: SecSha256Open, SecSha256Add h, piece, then SecSha256End$(h), which also frees the handle.
    -  SecHmac$(key, bytes)                                The HMAC-SHA256 signature of bytes with key; SecHmacOpen(key), SecHmacAdd and SecHmacEnd$ for pieces.
    +  SecHmac$(key, bytes)                                The HMAC-SHA256 signature of bytes with key; SecHmacOpen(key), SecHmacAdd, and SecHmacEnd$ for pieces.
       SecHkdf$(salt, secret, info, length)                length bytes (up to SEC_HKDF_MAX_OUT) of keys made from secret; a different info gives different keys.
       SecEqual(a, b)                                      True when a and b hold the same bytes, compared in the same time wherever they differ (for signature checks).
     .endtable
    @@ -5879,7 +5981,7 @@ Handles a program leaves open are freed, and their keys wiped, when it ends.
     
     .h2 Settings Files
     
    -With prefs.bas in the project, a program keeps its settings in an INI file the way DVX's own programs do. PrefsLoad(path) reads the file once (a file that is not there reads as empty); PrefsGet$, PrefsGetInt and PrefsGetBool read a key of a section (each with the value to use when it is missing), and PrefsSet, PrefsSetInt, PrefsSetBool and PrefsRemove change it in memory. PrefsSave writes it back, keeping comments and every line it did not change. To write a whole new file -- dropping sections that are no longer wanted -- fill a PrefsCreate handle and save it with PrefsSaveAs: the old file is replaced only once the new one is completely written. PrefsClose frees a handle. IniRead and IniWrite (built in) do one value at a time and read or write the whole file each time; prefs.bas is the faster choice for many settings.
    +With prefs.bas in the project, a program keeps its settings in an INI file the way DVX's own programs do. PrefsLoad(path) reads the file once (a file that is not there reads as empty); PrefsGet$, PrefsGetInt, and PrefsGetBool read a key of a section (each with the value to use when it is missing), and PrefsSet, PrefsSetInt, PrefsSetBool, and PrefsRemove change it in memory. PrefsSave writes it back, keeping comments and every line it did not change. To write a whole new file -- dropping sections that are no longer wanted -- fill a PrefsCreate handle and save it with PrefsSaveAs: the old file is replaced only once the new one is completely written. PrefsClose frees a handle. IniRead and IniWrite (built in) do one value at a time and read or write the whole file each time; prefs.bas is the faster choice for many settings.
     
     .code
     Dim ini As Long
    @@ -5890,9 +5992,9 @@ If Not PrefsSave(ini) Then MsgBox "The settings could not be saved."
     PrefsClose ini
     .endcode
     
    -PrefsDvxIni$() is the path of DVX's own settings file, DVX.INI, which Control Panel changes; other libraries (system.bas, joystick.bas, net.bas, print.bas and sound.bas) read and store their part of it through a prefs.bas handle. PrefsSetVideoMode handle, wide, high, bpp stores the screen mode DVX starts in, and PrefsVideoWidth(handle), PrefsVideoHeight(handle) and PrefsVideoBpp(handle) read it (DVX's own start-up mode, SYS_VIDEO_DEFAULT_* in system.bas, for a value that is not there). PrefsKeyName$(handle, section, index) is the name of key number index of a section (from 0, in the order of the file; "" past the last key), to go through every key of a section.
    +PrefsDvxIni$() is the path of DVX's own settings file, DVX.INI, which Control Panel changes; other libraries (system.bas, joystick.bas, net.bas, print.bas, and sound.bas) read and store their part of it through a prefs.bas handle. PrefsSetVideoMode handle, wide, high, bpp stores the screen mode DVX starts in, and PrefsVideoWidth(handle), PrefsVideoHeight(handle), and PrefsVideoBpp(handle) read it (DVX's own start-up mode, SYS_VIDEO_DEFAULT_* in system.bas, for a value that is not there). PrefsKeyName$(handle, section, index) is the name of key number index of a section (from 0, in the order of the file; "" past the last key), to go through every key of a section.
     
    -Some settings are stored by name, from a fixed list of choices: the mouse acceleration (PREFS_CHOICE_ACCEL), the wallpaper mode (PREFS_CHOICE_WALLPAPER) and the scroll wheel direction (PREFS_CHOICE_WHEEL). For each list, PrefsChoiceCount(table) says how many choices it has (numbered from 0), PrefsChoiceLabel$(table, i) is the text to show, PrefsChoiceName$(table, i) the name stored in the file and PrefsChoiceValue(table, i) the value it stands for (what SysSetMouse takes). PrefsChoiceIndex(table, choiceName) and PrefsChoiceIndexOfValue(table, value) find a choice (-1 when there is none), and PrefsGetChoice(handle, section, key, table) reads one from a file, giving DVX's default choice when the key is missing or unknown. PrefsChoiceDefault(table) is that default choice, what a Defaults button selects. For a list of your own, PrefsGetChoiceFrom(handle, section, key, choiceNames, defaultName) does the same with the names separated by "|" ("large|small|list" stores choice 1 as "small"), giving defaultName's choice for a missing or unknown key.
    +Some settings are stored by name, from a fixed list of choices: the mouse acceleration (PREFS_CHOICE_ACCEL), the wallpaper mode (PREFS_CHOICE_WALLPAPER), and the scroll wheel direction (PREFS_CHOICE_WHEEL). For each list, PrefsChoiceCount(table) says how many choices it has (numbered from 0), PrefsChoiceLabel$(table, i) is the text to show, PrefsChoiceName$(table, i) the name stored in the file, and PrefsChoiceValue(table, i) the value it stands for (what SysSetMouse takes). PrefsChoiceIndex(table, choiceName) and PrefsChoiceIndexOfValue(table, value) find a choice (-1 when there is none), and PrefsGetChoice(handle, section, key, table) reads one from a file, giving DVX's default choice when the key is missing or unknown. PrefsChoiceDefault(table) is that default choice, what a Defaults button selects. For a list of your own, PrefsGetChoiceFrom(handle, section, key, choiceNames, defaultName) does the same with the names separated by "|" ("large|small|list" stores choice 1 as "small"), giving defaultName's choice for a missing or unknown key.
     
     .code
     For i = 0 To PrefsChoiceCount(PREFS_CHOICE_ACCEL) - 1
    @@ -5905,9 +6007,9 @@ cboAccel.ListIndex = PrefsChoiceIndexOfValue(PREFS_CHOICE_ACCEL, SysMouseAccel()
     
     With help.bas in the project, HelpCompile(source, output) compiles a help source file (.dhs) into a help file (.hlp), True when it worked; HelpCompileMessages$() gives the compiler's errors and warnings, one per line, each with the file and line ("error: NOTES.DHS:1: directive .h1 outside of a topic"). HelpView file opens a help file in the DVX Help Viewer (a file named without a folder is looked for beside the program; "" opens the DVX user guide). HelpViewTopic file, topicId opens it at a topic, the way F1 does. A Help Viewer that is already open shows the file in its window instead of a second copy starting.
     
    -With hlpfile.bas, a program reads help files itself -- everything a help viewer needs. HlpOpen(path) opens a help file and returns a handle (0 when it is not one); HlpClose closes it (a program's open files are closed when it ends). Topics are numbered from 0: HlpTopicCount, HlpTopicTitle$ and HlpTopicId$ (the name links and HelpTopic use), HlpFindTopic(handle, topicId) (-1 when there is none) and HlpStartTopic(handle, topicId), the topic to show first (topicId when the file has it, else the file's own start page). HlpTocCount, HlpTocTitle$, HlpTocDepth, HlpTocTopic and HlpTocExpanded give the table of contents in order -- a depth of 0 is the top level, a topic of -1 a heading that only groups others -- ready to fill a TreeView. HlpIndexCount, HlpIndexKeyword$ and HlpIndexTopic give the keyword index (sorted), for a list or DlgChoice. HlpSearch(handle, words) searches the titles, text and keywords as the Help Viewer does and returns how many topics have every word; HlpSearchTopic(handle, n) gives them.
    +With hlpfile.bas, a program reads help files itself -- everything a help viewer needs. HlpOpen(path) opens a help file and returns a handle (0 when it is not one); HlpClose closes it (a program's open files are closed when it ends). Topics are numbered from 0: HlpTopicCount, HlpTopicTitle$ and HlpTopicId$ (the name links and HelpTopic use), HlpFindTopic(handle, topicId) (-1 when there is none), and HlpStartTopic(handle, topicId), the topic to show first (topicId when the file has it, else the file's own start page). HlpTocCount, HlpTocTitle$, HlpTocDepth, HlpTocTopic, and HlpTocExpanded give the table of contents in order -- a depth of 0 is the top level, a topic of -1 a heading that only groups others -- ready to fill a TreeView. HlpIndexCount, HlpIndexKeyword$, and HlpIndexTopic give the keyword index (sorted), for a list or DlgChoice. HlpSearch(handle, words) searches the titles, text, and keywords as the Help Viewer does and returns how many topics have every word; HlpSearchTopic(handle, n) gives them.
     
    -To show a topic, put a HelpPane control on the form and call HelpPane1.ShowTopic handle, topic: headings, paragraphs, lists, notes, code, tables, pictures and links look as they do in the Help Viewer, and the text wraps to the control's width. A click on a link, or Enter on one reached with Tab, fires the HelpPane's LinkClick event with the ID of the topic the link names; the program decides what to do, usually show that topic and remember it for a Back button. A program that draws topics itself reads them with HlpLoadTopic(handle, topic) and HlpRecordType, HlpRecordFlags, HlpRecordText$ and HlpRecordLink$ (the HLP_REC_ constants), saves a picture for LOADPICTURE with HlpRecordImageSave, and wraps text with HlpWrap$.
    +To show a topic, put a HelpPane control on the form and call HelpPane1.ShowTopic handle, topic: headings, paragraphs, lists, notes, code, tables, pictures, and links look as they do in the Help Viewer, and the text wraps to the control's width. A click on a link, or Enter on one reached with Tab, fires the HelpPane's LinkClick event with the ID of the topic the link names; the program decides what to do, usually show that topic and remember it for a Back button. A program that draws topics itself reads them with HlpLoadTopic(handle, topic) and HlpRecordType, HlpRecordFlags, HlpRecordText$, and HlpRecordLink$ (the HLP_REC_ constants), saves a picture for LOADPICTURE with HlpRecordImageSave, and wraps text with HlpWrap$.
     
     .code
     Dim hlp As Long
    @@ -5958,15 +6060,15 @@ lstFiles.MaxHeight = SizePercent(50)
     
     With compiler.bas in the project, a program compiles BASIC programs -- what a development tool written in BASIC needs. A source set holds the files of one program: CompSourceFromProject(src, dbpPath) reads a whole project (.dbp) from disk, or CompSourceAdd(src, path, text, kind) adds files one at a time, in the order a project compiles: modules, then class modules, then UserControls, then forms (COMP_KIND_AUTO tells them apart by the file name; a form's whole .frm text is added, and the code after its layout is compiled; a UserControl's whole .ctl text, its header and its code). CompCompile(src, optionExplicit, validate) gives a compiled module, or 0 with CompError$() saying what is wrong and CompErrorFile$() / CompErrorFileLine() where. With validate True, Label1.Captoin and other misspelled control members, and controls of an unknown type, are found while compiling instead of when the line runs (the set's own UserControls are known types).
     
    -Line numbers of compiled programs (errors, breakpoints, the line a program is paused on) count through all the files of the source set. CompMapFile$(src, line) and CompMapLine(src, line) give the file and the line in it; CompConcatLine(src, path, fileLine) goes the other way. CompSourceFiles$ lists the files in compile order. CompBuildApp(module, src, outPath, release) builds an .app the way the IDE's Make Executable does, with the project's name, icon and help file, its forms and its UserControls. For a control project (its Type setting is "Control") it builds a control file instead (outPath NAME.wgt), the way the IDE's Make Control does: the project's UserControls with their toolbox pictures and names, there for every program once the file is copied into DVX's WIDGETS folder (DVX loads it when it starts). CompProcList$(text) lists the SUB and FUNCTION blocks of source text with their first and last lines (also for code that does not compile yet), CompIsIdent checks a name and CompKeyword$ lists the language's keywords, CompKeywordClass the kind of each (COMP_KW_OTHER, COMP_KW_TYPE or COMP_KW_LITERAL) and CompKeywordReserved whether a program may also use it as a name of its own.
    +Line numbers of compiled programs (errors, breakpoints, the line a program is paused on) count through all the files of the source set. CompMapFile$(src, line) and CompMapLine(src, line) give the file and the line in it; CompConcatLine(src, path, fileLine) goes the other way. CompSourceFiles$ lists the files in compile order. CompBuildApp(module, src, outPath, release) builds an .app the way the IDE's Make Executable does, with the project's name, icon, and help file, its forms, and its UserControls. For a control project (its Type setting is "Control") it builds a control file instead (outPath NAME.wgt), the way the IDE's Make Control does: the project's UserControls with their toolbox pictures and names, there for every program once the file is copied into DVX's WIDGETS folder (DVX loads it when it starts). CompProcList$(text) lists the SUB and FUNCTION blocks of source text with their first and last lines (also for code that does not compile yet), CompIsIdent checks a name and CompKeyword$ lists the language's keywords, CompKeywordClass the kind of each (COMP_KW_OTHER, COMP_KW_TYPE, or COMP_KW_LITERAL), and CompKeywordReserved whether a program may also use it as a name of its own.
     
    -Project files: ProjOpen(dbpPath) opens a project file (.dbp) and ProjNew(name, folder) starts a new one; ProjSave writes it and ProjClose frees it. ProjFileCount, ProjFile$ (the name as the project lists it), ProjFilePath$ (the full path), ProjFileKind (COMP_KIND_MODULE, COMP_KIND_FORM, COMP_KIND_CLASS or COMP_KIND_CONTROL) and ProjFileFind list its files; ProjAddFile, ProjRemoveFile and ProjRenameFile (on disk and in the project together, saved at once) change the list. ProjGet$ and ProjSet read and set its settings by their names in the file: Name, Type ("Control" for a control project, empty for a program), Author, Publisher, Version, Copyright, Description, Icon, HelpFile, CommandArgs, StartupForm, OptionExplicit and MultiInstance (the last two "true" or "false"). This is the project model the DVX BASIC IDE uses.
    +Project files: ProjOpen(dbpPath) opens a project file (.dbp) and ProjNew(name, folder) starts a new one; ProjSave writes it and ProjClose frees it. ProjFileCount, ProjFile$ (the name as the project lists it), ProjFilePath$ (the full path), ProjFileKind (COMP_KIND_MODULE, COMP_KIND_FORM, COMP_KIND_CLASS, or COMP_KIND_CONTROL), and ProjFileFind list its files; ProjAddFile, ProjRemoveFile, and ProjRenameFile (on disk and in the project together, saved at once) change the list. ProjGet$ and ProjSet read and set its settings by their names in the file: Name, Type ("Control" for a control project, empty for a program), Author, Publisher, Version, Copyright, Description, Icon, HelpFile, CommandArgs, StartupForm, OptionExplicit, and MultiInstance (the last two "true" or "false"). This is the project model the DVX BASIC IDE uses.
     
    -A UserControl's header (the Begin UserControl block at the top of its .ctl file) is read and changed the way the IDE's Properties window does it, the rest of the text left as it is: ProjCtlKeys$() lists the keys in the window's order with the kind of value each takes (Name, Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, DefaultEvent; the kinds Name, Integer, Boolean, Color and Text), ProjCtlHeader$(text, key) reads one (its default when the header leaves it out), ProjCtlSetHeader$(text, key, value) gives the text with one set (an empty value removes it; Name renames the control) and ProjCtlNew$(name) the text of a new UserControl, as File > New UserControl makes it.
    +A UserControl's header (the Begin UserControl block at the top of its .ctl file) is read and changed the way the IDE's Properties window does it, the rest of the text left as it is: ProjCtlKeys$() lists the keys in the window's order with the kind of value each takes (Name, Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, DefaultEvent; the kinds Name, Integer, Boolean, Color, and Text), ProjCtlHeader$(text, key) reads one (its default when the header leaves it out), ProjCtlSetHeader$(text, key, value) gives the text with one set (an empty value removes it; Name renames the control), and ProjCtlNew$(name) the text of a new UserControl, as File > New UserControl makes it.
     
    -With debug.bas too, a program runs a compiled module as a child program inside itself: RunCreate(module, src, appDir, args) prepares it, RunStart(run, startupForm, breakOnStart) loads its forms, and RunPoll(run, ms), called from a Timer, runs it for at most ms milliseconds at a time and returns what it is doing: RUN_RUNNING, RUN_EVENTS (waiting for events on its forms), RUN_PAUSED, RUN_ENDED or RUN_ERROR (RunError$ and RunErrorLine tell why and where). RunOutput$ collects what it PRINTs; INPUT asks with an input box. RunStop ends it and RunFree forgets it.
    +With debug.bas too, a program runs a compiled module as a child program inside itself: RunCreate(module, src, appDir, args) prepares it, RunStart(run, startupForm, breakOnStart) loads its forms, and RunPoll(run, ms), called from a Timer, runs it for at most ms milliseconds at a time and returns what it is doing: RUN_RUNNING, RUN_EVENTS (waiting for events on its forms), RUN_PAUSED, RUN_ENDED, or RUN_ERROR (RunError$ and RunErrorLine tell why and where). RunOutput$ collects what it PRINTs; INPUT asks with an input box. RunStop ends it and RunFree forgets it.
     
    -DbgSetBreakpoints run, "12, 40" sets breakpoints (line numbers of the source set). While the program is paused, DbgLine and DbgProc$ say where, DbgLocals$ and DbgCallStack$ give the variables and the calls as rows for a ListView's SetRows, DbgEval$(run, expr) shows a variable or expression as the IDE's Watch window does, DbgExec$(run, line) runs an Immediate-window line (x = 5 changes a variable, anything else is printed) and DbgSetVar changes a variable; DbgStepInto, DbgStepOver, DbgStepOut, DbgRunToLine and DbgContinue go on, DbgPause stops it at the next line. The program's form captions show [break] while it is paused.
    +DbgSetBreakpoints run, "12, 40" sets breakpoints (line numbers of the source set). While the program is paused, DbgLine and DbgProc$ say where, DbgLocals$ and DbgCallStack$ give the variables and the calls as rows for a ListView's SetRows, DbgEval$(run, expr) shows a variable or expression as the IDE's Watch window does, DbgExec$(run, line) runs an Immediate-window line (x = 5 changes a variable, anything else is printed), and DbgSetVar changes a variable; DbgStepInto, DbgStepOver, DbgStepOut, DbgRunToLine, and DbgContinue go on, DbgPause stops it at the next line. The program's form captions show [break] while it is paused.
     
     A program paused in its module-level code waits until you step or continue it. One paused inside one of its event handlers -- or in its forms' Load code, which runs inside RunStart -- waits right there while your Timer and menus keep working; RunPoll called meanwhile returns at once, and RunStart returns only after such a pause.
     
    @@ -5998,11 +6100,11 @@ End Sub
     
     .h2 Form Designers and Code Editors
     
    -A form designer or a code editor written in BASIC uses the same pieces as the DVX BASIC IDE. designer.bas describes the control types, which CtlTypeName$(0 to CtlTypeCount - 1) gives in alphabetical order: the toolbox lists the types CtlTypeFlags marks CTL_FLAG_DESIGNABLE (with CtlTypeTooltip$ and the icon CtlTypeIconFile writes), new controls are named CtlTypeNamePrefix$ plus a number, and the properties window lists CtlPropName$ for 0 to CtlPropCount - 1 with CtlPropType (CTL_PROP_STRING ... CTL_PROP_ENUM, plus CTL_PROP_READONLY) and CtlPropEnum$ for an enumerated property's choices. "Form" and "Menu" stand for the form itself and a menu item. The event list is CtlEventList$, and CtlEventParams$ writes an event procedure's parameter list; CtlTypeEventParams$(typeName, eventName) does it for one type, whose events may have parameters of their own (a UserControl's events), and "UserControl" stands for the object in a UserControl's own code (UserControl_MouseDown has Button, Shift, X and Y). For a project's files, FrmFormName$ and FrmControlNames$ read a .frm file's form and control names (the Object list), FrmCode$ its code, and ClsClassName$ a class module's name.
    +A form designer or a code editor written in BASIC uses the same pieces as the DVX BASIC IDE. designer.bas describes the control types, which CtlTypeName$(0 to CtlTypeCount - 1) gives in alphabetical order: the toolbox lists the types CtlTypeFlags marks CTL_FLAG_DESIGNABLE (with CtlTypeTooltip$ and the icon CtlTypeIconFile writes), new controls are named CtlTypeNamePrefix$ plus a number, and the properties window lists CtlPropName$ for 0 to CtlPropCount - 1 with CtlPropType (CTL_PROP_STRING ... CTL_PROP_ENUM, plus CTL_PROP_READONLY) and CtlPropEnum$ for an enumerated property's choices. "Form" and "Menu" stand for the form itself and a menu item. The event list is CtlEventList$, and CtlEventParams$ writes an event procedure's parameter list; CtlTypeEventParams$(typeName, eventName) does it for one type, whose events may have parameters of their own (a UserControl's events), and "UserControl" stands for the object in a UserControl's own code (UserControl_MouseDown has Button, Shift, X, and Y). For a project's files, FrmFormName$ and FrmControlNames$ read a .frm file's form and control names (the Object list), FrmCode$ its code, and ClsClassName$ a class module's name.
     
     The form being designed is a real form: CreateControl and RemoveControl add and remove controls, CallByName reads and sets a property by its name, Me.Controls lists the controls, Container (Set ctl.Container = Frame1) and MoveBefore move them, and LoadFormText shows a .frm file. Its DesignMode (see the Form control) stops the controls from working and reports the mouse and keys in DesignMouse and DesignKey, with the selection handles drawn for DesignSelection.
     
    -A project's own UserControls go on the toolbox too, as in the IDE. Compile the project -- its forms may be left out -- and give the module and the source set to CompDesignControls(module, src, appDir) from compiler.bas: the UserControls are then control types of this program alone, listed by CtlTypeName$ with their Description as the tooltip and their ToolboxIcon as the icon, with their properties, methods and events, and known to CreateControl and LoadFormText. On the forms they run their code -- not the project's module-level code -- with UserControl.UserMode False on a form in DesignMode; App.Path is appDir, or the project's folder for "". When a control's code calls UserControl.PropertyChanged, the form raises DesignPropertyChanged (CtrlName, PropertyName), so the properties window can read them again. Call CompDesignControls again after a UserControl changed (the ones before are let go first) and CompDesignRelease when the project closes; the end of the program lets them go too. The module stays in use while they are there, even after CompModuleFree, and must be one this program compiled.
    +A project's own UserControls go on the toolbox too, as in the IDE. Compile the project -- its forms may be left out -- and give the module and the source set to CompDesignControls(module, src, appDir) from compiler.bas: the UserControls are then control types of this program alone, listed by CtlTypeName$ with their Description as the tooltip and their ToolboxIcon as the icon, with their properties, methods, and events, and known to CreateControl and LoadFormText. On the forms they run their code -- not the project's module-level code -- with UserControl.UserMode False on a form in DesignMode; App.Path is appDir, or the project's folder for "". When a control's code calls UserControl.PropertyChanged, the form raises DesignPropertyChanged (CtrlName, PropertyName), so the properties window can read them again. Call CompDesignControls again after a UserControl changed (the ones before are let go first) and CompDesignRelease when the project closes; the end of the program lets them go too. The module stays in use while they are there, even after CompModuleFree, and must be one this program compiled.
     
     .code
     ' compiler.bas and designer.bas in the project; Canvas is the form
    @@ -6028,13 +6130,13 @@ Sub Canvas_DesignPropertyChanged(CtrlName As String, PropertyName As String)
     End Sub
     .endcode
     
    -A .frm file can also be worked on as data, the way the DVX BASIC IDE does it, without showing it. FrmOpen(text$), FrmOpenFile(path$) or FrmNew(name$) gives a handle to a form model (0 when the text is not a form); FrmText$ writes it back as .frm text in the form the IDE saves and FrmSave writes it to a file; FrmFree frees it (a program's open handles are freed when it ends). FrmGet$ and FrmSet read and change the form's properties (Caption, Width, AutoSize, Layout, Icon, Tag ...; Name renames the form), FrmGetCode$ and FrmSetCode its code. The controls are numbered from 0 in file order, each one followed by the controls inside it: FrmCtlCount, FrmCtlFind, FrmCtlName$, FrmCtlType$, FrmCtlIndex (its control array index, -1 when it is not in an array) and FrmCtlParent (the container, -1 for the form) describe them; FrmCtlGet$ and FrmCtlSet read and set any property as text (Name renames the control and its whole control array, the controls inside it follow; Index makes it an array member; an OptionButton set to True clears the others in its container), FrmCtlRemoveProp takes a set one out, and FrmCtlPropCount / FrmCtlPropName$ list the ones that were set. FrmCtlAdd adds a control (its name "" gives the next free one, Command1, Label2 ...) as the last control of a container (-1: the form), FrmCtlRemove removes one with what is inside it, FrmCtlMove puts one before another of the same container and FrmCtlReparent into another container. FrmCtlText$ gives a control with what is inside it as .frm text, what a designer puts on the Clipboard to copy it, and FrmCtlPaste puts such text into a container: a name that is already taken is replaced by a free one. The menu items are numbered from 0 in menu order with a Level (0 for a menu on the menu bar or a popup menu, 1 for its items, 2 and more for submenus): FrmMenuCount, FrmMenuGet$ / FrmMenuSet (Name, Caption, Shortcut, ShortcutText, Checked, RadioCheck, Enabled, Visible, Level), FrmMenuAdd, FrmMenuRemove (with the items under it), FrmMenuMove (past the previous or next item of its level) and FrmMenuIndent (one level in or out, with the items under it). Only what was set is written: a property a control has by default is left out of the file.
    +A .frm file can also be worked on as data, the way the DVX BASIC IDE does it, without showing it. FrmOpen(text$), FrmOpenFile(path$), or FrmNew(name$) gives a handle to a form model (0 when the text is not a form); FrmText$ writes it back as .frm text in the form the IDE saves and FrmSave writes it to a file; FrmFree frees it (a program's open handles are freed when it ends). FrmGet$ and FrmSet read and change the form's properties (Caption, Width, AutoSize, Layout, Icon, Tag ...; Name renames the form), FrmGetCode$ and FrmSetCode its code. The controls are numbered from 0 in file order, each one followed by the controls inside it: FrmCtlCount, FrmCtlFind, FrmCtlName$, FrmCtlType$, FrmCtlIndex (its control array index, -1 when it is not in an array), and FrmCtlParent (the container, -1 for the form) describe them; FrmCtlGet$ and FrmCtlSet read and set any property as text (Name renames the control and its whole control array, the controls inside it follow; Index makes it an array member; an OptionButton set to True clears the others in its container), FrmCtlRemoveProp takes a set one out, and FrmCtlPropCount / FrmCtlPropName$ list the ones that were set. FrmCtlAdd adds a control (its name "" gives the next free one, Command1, Label2 ...) as the last control of a container (-1: the form), FrmCtlRemove removes one with what is inside it, FrmCtlMove puts one before another of the same container, and FrmCtlReparent into another container. FrmCtlText$ gives a control with what is inside it as .frm text, what a designer puts on the Clipboard to copy it, and FrmCtlPaste puts such text into a container: a name that is already taken is replaced by a free one. The menu items are numbered from 0 in menu order with a Level (0 for a menu on the menu bar or a popup menu, 1 for its items, 2 and more for submenus): FrmMenuCount, FrmMenuGet$ / FrmMenuSet (Name, Caption, Shortcut, ShortcutText, Checked, RadioCheck, Enabled, Visible, Level), FrmMenuAdd, FrmMenuRemove (with the items under it), FrmMenuMove (past the previous or next item of its level), and FrmMenuIndent (one level in or out, with the items under it). Only what was set is written: a property a control has by default is left out of the file.
     
     FormText$(formName$) writes a loaded form as it is now as .frm text -- one shown with LoadFormText, made with CreateForm and CreateControl, or one of the program's own: its properties, its controls in their containers with each property that differs from a new control of that type, and its menus (a form loaded this way has no code). A designer that keeps its form live saves it with FormText$, and FrmOpen(FormText$(...)) reads it as a model.
     
    -compiler.bas reads source text as the compiler does, for a code editor: CompTokens$(source) lists its tokens (line, column, length, kind and text, the kind one of Keyword, Identifier, String, Number, Date, Symbol, Comment and Error); CompInCode(source, position) tells whether a character is code rather than inside a string or a comment; CompRenameInCode$(source, oldName, newName, codeOnly) renames a form, control or menu item where code refers to it (Command1.Caption, Command1_Click), leaving strings and comments alone with codeOnly; CompWithEvents$ lists the WithEvents variables a file declares and CompClassEvents$ the Events of a class module, for the Object and Event lists; CompHelpTopic$(word) and CompHelpCtrlTopic$(typeName) are the help topics F1 opens (HelpViewTopic on dvxbasic.hlp beside the IDE).
    +compiler.bas reads source text as the compiler does, for a code editor: CompTokens$(source) lists its tokens (line, column, length, kind, and text, the kind one of Keyword, Identifier, String, Number, Date, Symbol, Comment, and Error); CompInCode(source, position) tells whether a character is code rather than inside a string or a comment; CompRenameInCode$(source, oldName, newName, codeOnly) renames a form, control, or menu item where code refers to it (Command1.Caption, Command1_Click), leaving strings and comments alone with codeOnly; CompWithEvents$ lists the WithEvents variables a file declares and CompClassEvents$ the Events of a class module, for the Object and Event lists; CompHelpTopic$(word) and CompHelpCtrlTopic$(typeName) are the help topics F1 opens (HelpViewTopic on dvxbasic.hlp beside the IDE).
     
    -A code editor is a TextArea: SetSyntaxMode "bas" colours BASIC as the IDE does (SetSyntaxColor changes the colours), SetLineMarker marks breakpoints and the current line, GutterClick reports clicks on the line numbers, SelStart / SelLength / SelText and Cut, Copy, Paste, SelectAll and Undo give the Edit menu, FindNext / ReplaceSelection / ReplaceAll find and replace, and LineCount / GetLine / CursorLine / CursorCol read the text (see TextArea).
    +A code editor is a TextArea: SetSyntaxMode "bas" colours BASIC as the IDE does (SetSyntaxColor changes the colours), SetLineMarker marks breakpoints and the current line, GutterClick reports clicks on the line numbers, SelStart / SelLength / SelText and Cut, Copy, Paste, SelectAll, and Undo give the Edit menu, FindNext / ReplaceSelection / ReplaceAll find and replace, and LineCount / GetLine / CursorLine / CursorCol read the text (see TextArea).
     
     .code
     ' designer.bas in the project: put a copy of the first control of a
    diff --git a/src/apps/kpunch/dvxbasic/test_compiler.c b/src/apps/kpunch/dvxbasic/test_compiler.c
    index 0ea24f4d..18be5ad9 100644
    --- a/src/apps/kpunch/dvxbasic/test_compiler.c
    +++ b/src/apps/kpunch/dvxbasic/test_compiler.c
    @@ -5795,7 +5795,7 @@ static void testObjectsAndVariants(void) {
     
         expectCompileError("NEW of a non-class rejected",
             "x = NEW Foo\n",
    -        "class name, form name or Collection after New"
    +        "class name, form name, or Collection after New"
         );
     
         // New FormName: a copy of a form of the project (headless, the
    @@ -10932,7 +10932,7 @@ int main(void) {
                 char name[64];
     
                 snprintf(name, sizeof(name), "Procedure inside a block rejected %d", (int)i);
    -            expectCompileError(name, procInBlock[i], "cannot be defined inside IF, FOR, DO, WHILE, SELECT CASE or WITH");
    +            expectCompileError(name, procInBlock[i], "cannot be defined inside IF, FOR, DO, WHILE, SELECT CASE, or WITH");
             }
         }
     
    @@ -11452,7 +11452,7 @@ int main(void) {
     
         expectCompileError("EXIT of a non-loop rejected",
             "EXIT PRINT\n",
    -        "Expected FOR, DO, SUB, FUNCTION or PROPERTY after EXIT"
    +        "Expected FOR, DO, SUB, FUNCTION, or PROPERTY after EXIT"
         );
     
         compileOnly("GET and PUT of a whole UDT record",
    diff --git a/src/apps/kpunch/dvxbasic/userctl.dhs b/src/apps/kpunch/dvxbasic/userctl.dhs
    index ce24446d..0d3b6563 100644
    --- a/src/apps/kpunch/dvxbasic/userctl.dhs
    +++ b/src/apps/kpunch/dvxbasic/userctl.dhs
    @@ -31,7 +31,7 @@
     
     .h1 UserControls: Your Own Controls
     
    -A UserControl is a control you write in DVX BASIC. Once written, it is used exactly like the controls DVX comes with: you put it on a form in the designer (or in a .frm file with Begin), set its properties, call its methods and write handlers for its events. Programs written in C can create and use it too.
    +A UserControl is a control you write in DVX BASIC. Once written, it is used exactly like the controls DVX comes with: you put it on a form in the designer (or in a .frm file with Begin), set its properties, call its methods, and write handlers for its events. Programs written in C can create and use it too.
     
     A UserControl lives in a .ctl file. In the IDE, File > New UserControl makes one (see UserControls in the IDE). The file starts with a header that describes the control, then holds the control's code. The code is a class: every control on a form is one object of the class, with its own copies of the class's variables.
     
    @@ -97,15 +97,15 @@ The header is a Begin UserControl block: the control's name (the type name forms
       DefaultEvent   The event the designer opens when the control is double-clicked.
     .endtable
     
    -.h2 Properties, Methods and Events
    +.h2 Properties, Methods, and Events
     
     The control's public members are what programs use:
     
     .list
    -.item Public Property Get / Let (or Set) pairs and Public variables are its properties. A property with only Property Get is read-only, with only Property Let write-only. A property whose type is Integer, Long or Byte is a whole number, Boolean is True / False, Single and Double (and Currency) are numbers, String, Date and Variant are text; a property of an object type, an array or a TYPE cannot be a property of a control.
    -.item A property declared As an ENUM is a choice of its members, as a built-in control's Alignment is: the designer's Properties window shows the member's name and a double-click steps to the next member, the form file stores the name (Look = Circle), and code sets it with a member, its number or its name as text (Shape1.Look = Circle, Shape1.Look = 1, Shape1.Look = "Circle"). This needs the members numbered 0, 1, 2 and so on, each number once, as an ENUM numbers them when no values are given; an ENUM with other values (Small = 10) makes a whole-number property.
    +.item Public Property Get / Let (or Set) pairs and Public variables are its properties. A property with only Property Get is read-only, with only Property Let write-only. A property whose type is Integer, Long, or Byte is a whole number, Boolean is True / False, Single and Double (and Currency) are numbers, String, Date, and Variant are text; a property of an object type, an array, or a TYPE cannot be a property of a control.
    +.item A property declared As an ENUM is a choice of its members, as a built-in control's Alignment is: the designer's Properties window shows the member's name and a double-click steps to the next member, the form file stores the name (Look = Circle), and code sets it with a member, its number, or its name as text (Shape1.Look = Circle, Shape1.Look = 1, Shape1.Look = "Circle"). This needs the members numbered 0, 1, 2, and so on, each number once, as an ENUM numbers them when no values are given; an ENUM with other values (Small = 10) makes a whole-number property.
     .item Public Subs and Functions are its methods. They take any arguments; a Function's value is the method's result.
    -.item Public Events are its events. RaiseEvent sends one to the program that has the control: its handler CtrlName_EventName runs with the arguments, and a ByRef parameter the handler changes (a number, a Boolean or a String) comes back changed.
    +.item Public Events are its events. RaiseEvent sends one to the program that has the control: its handler CtrlName_EventName runs with the arguments, and a ByRef parameter the handler changes (a number, a Boolean, or a String) comes back changed.
     .endlist
     
     Names that every control already has (Name, Left, Top, Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight, Weight, Visible, Enabled, ReadOnly, BackColor, ForeColor, Caption, Text, ToolTipText, ContextMenu, HelpTopic, DataSource, DataField, Tag, MousePointer ...) belong to the form: a control's own property of such a name is not used. Give your properties other names (Title instead of Caption, Value or Level instead of Text).
    @@ -137,7 +137,7 @@ Inside the control's code, UserControl is the control itself: the surface it dra
     
     The drawing members work exactly as on a PictureBox (see Drawing on Forms and PictureBoxes). Write them with UserControl in front -- UserControl.Line, UserControl.Print -- as a form's code writes Me.Line: a drawing statement on its own (Line, Cls, Print) draws on the program's screen, not on the control.
     
    -The drawing stays until you draw again. UserControl_Paint runs when the control first appears, after its size changed, when you call Refresh and after you call Invalidate; draw everything there and call UserControl.Invalidate (or UserControl.Refresh, to see the result at once) whenever what it shows changes.
    +The drawing stays until you draw again. UserControl_Paint runs when the control first appears, after its size changed, when you call Refresh, and after you call Invalidate; draw everything there and call UserControl.Invalidate (or UserControl.Refresh, to see the result at once) whenever what it shows changes.
     
     .h2 The Control's Events
     
    @@ -163,9 +163,9 @@ The program runs these procedures of the control's code when they exist. Each is
       UserControl_LostFocus ()                                               It lost the keyboard focus.
     .endtable
     
    -Button is vbLeftButton, vbRightButton or vbMiddleButton (in MouseMove, the buttons held). Shift holds vbShiftMask, vbCtrlMask and vbAltMask. X and Y are pixels from the control's top-left corner. The compiler reports a handler with other parameters.
    +Button is vbLeftButton, vbRightButton, or vbMiddleButton (in MouseMove, the buttons held). Shift holds vbShiftMask, vbCtrlMask, and vbAltMask. X and Y are pixels from the control's top-left corner. The compiler reports a handler with other parameters.
     
    -These events are the control's own: the program does not see them. The program sees only the events the control raises. To let a program react to clicks, raise your own Click (Public Event Click, then RaiseEvent Click in UserControl_Click); an event named Click, DblClick, Change, KeyPress, KeyDown, KeyUp, MouseDown, MouseUp, MouseMove or Scroll reaches the program's CtrlName_Click (and so on) handler as for any control. Give those events Visual Basic's parameters: Button, Shift, X, Y for MouseDown, MouseUp and MouseMove; KeyCode, Shift for KeyDown and KeyUp; KeyAscii for KeyPress; Delta for Scroll. GotFocus and LostFocus reach the program by themselves.
    +These events are the control's own: the program does not see them. The program sees only the events the control raises. To let a program react to clicks, raise your own Click (Public Event Click, then RaiseEvent Click in UserControl_Click); an event named Click, DblClick, Change, KeyPress, KeyDown, KeyUp, MouseDown, MouseUp, MouseMove, or Scroll reaches the program's CtrlName_Click (and so on) handler as for any control. Give those events Visual Basic's parameters: Button, Shift, X, Y for MouseDown, MouseUp, and MouseMove; KeyCode, Shift for KeyDown and KeyUp; KeyAscii for KeyPress; Delta for Scroll. GotFocus and LostFocus reach the program by themselves.
     
     .h2 Using Controls in a Program
     
    @@ -207,7 +207,7 @@ All the programs using an installed control file share one copy of its code and
     
     .list
     .item A control cannot hold other controls; it draws what it shows on its UserControl surface.
    -.item Properties are numbers, Booleans, text and ENUM choices; a property of an ENUM whose members are not numbered 0, 1, 2 ... is a whole number in the Properties window.
    +.item Properties are numbers, Booleans, text, and ENUM choices; a property of an ENUM whose members are not numbered 0, 1, 2 ... is a whole number in the Properties window.
     .item CanGetFocus is fixed by the header (read-only in code).
     .item UserControl_Terminate cannot draw: the surface is already gone.
     .item A program cannot have a control of the same name as one installed in WIDGETS (the program stops with "Control cannot be registered").
    diff --git a/src/apps/kpunch/dvxdemo/dvxdemo.c b/src/apps/kpunch/dvxdemo/dvxdemo.c
    index 7da17b73..7b8ea00a 100644
    --- a/src/apps/kpunch/dvxdemo/dvxdemo.c
    +++ b/src/apps/kpunch/dvxdemo/dvxdemo.c
    @@ -38,6 +38,7 @@
     
     #include "dvxApp.h"
     #include "dvxDlg.h"
    +#include "dvxText.h"
     #include "dvxWgt.h"
     #include "ansiTerm/ansiTerm.h"
     #include "box/box.h"
    @@ -53,6 +54,8 @@
     #include "listView/listView.h"
     #include "progressBar/progress.h"
     #include "radio/radio.h"
    +#include "richEdit/richEdit.h"
    +#include "richLabel/richLbl.h"
     #include "scrollPane/scrlPane.h"
     #include "separator/separatr.h"
     #include "slider/slider.h"
    @@ -144,6 +147,12 @@
     #define DEMO_FORM_Y        200
     #define DEMO_FORM_W        280
     #define DEMO_FORM_H        360
    +#define DEMO_RICH_W        320      // the Fonts tab's rich text wraps at this width
    +#define DEMO_FONT_SAMPLE   "fontSample"
    +#define DEMO_RICH_EDIT     "richEdit"
    +#define DEMO_EDIT_FAMILY   "DejaVu Serif"
    +#define DEMO_EDIT_SIZE     20       // the rich edit box's heading
    +#define DEMO_EDIT_HEADING  10       // its heading's characters ("Type here.")
     #define DEMO_CANVAS_W      280
     #define DEMO_CANVAS_H      80
     #define DEMO_TEXT_MARGIN   4
    @@ -200,6 +209,55 @@ static const DemoInfoCmdT sInfoCmds[] = {
         { CMD_CTX_PROPS,       "Properties",   "Window properties dialog would appear here."  }
     };
     
    +// The Fonts tab's rich text: each run's font, style, color and text, then
    +// whether a paragraph (with its alignment) starts after it
    +typedef struct {
    +    const char *family;
    +    int32_t     size;
    +    int32_t     style;
    +    uint32_t    color;
    +    const char *text;
    +    int32_t     paragraph;      // FLOW_ALIGN_*, or DEMO_NO_PARAGRAPH
    +} DemoRunT;
    +
    +#define DEMO_NO_PARAGRAPH (-1)
    +#define DEMO_RED          DVX_RGB(160, 0, 0)
    +#define DEMO_BLUE         DVX_RGB(0, 0, 160)
    +
    +static const DemoRunT sRichRuns[] = {
    +    { "DejaVu Sans", 18, FONT_STYLE_BOLD,      RICHLABEL_COLOR_DEFAULT, "Rich text",                              FLOW_ALIGN_LEFT   },
    +    { "DejaVu Sans", 12, 0,                    RICHLABEL_COLOR_DEFAULT, "One control mixes ",                     DEMO_NO_PARAGRAPH },
    +    { "DejaVu Sans", 12, FONT_STYLE_BOLD,      RICHLABEL_COLOR_DEFAULT, "bold",                                   DEMO_NO_PARAGRAPH },
    +    { "DejaVu Sans", 12, 0,                    RICHLABEL_COLOR_DEFAULT, ", ",                                     DEMO_NO_PARAGRAPH },
    +    { "DejaVu Sans", 12, FONT_STYLE_ITALIC,    RICHLABEL_COLOR_DEFAULT, "italic",                                 DEMO_NO_PARAGRAPH },
    +    { "DejaVu Sans", 12, 0,                    RICHLABEL_COLOR_DEFAULT, ", ",                                     DEMO_NO_PARAGRAPH },
    +    { "DejaVu Sans", 12, FONT_STYLE_UNDERLINE, RICHLABEL_COLOR_DEFAULT, "underlined",                             DEMO_NO_PARAGRAPH },
    +    { "DejaVu Sans", 12, 0,                    RICHLABEL_COLOR_DEFAULT, " and ",                                  DEMO_NO_PARAGRAPH },
    +    { "DejaVu Sans", 12, FONT_STYLE_STRIKE,    RICHLABEL_COLOR_DEFAULT, "struck out",                             DEMO_NO_PARAGRAPH },
    +    { "DejaVu Sans", 12, 0,                    RICHLABEL_COLOR_DEFAULT, " text, ",                                DEMO_NO_PARAGRAPH },
    +    { "DejaVu Sans", 12, 0,                    DEMO_RED,                "colors",                                 DEMO_NO_PARAGRAPH },
    +    { "DejaVu Sans", 12, 0,                    RICHLABEL_COLOR_DEFAULT, ", ",                                     DEMO_NO_PARAGRAPH },
    +    { "Tinos",       20, 0,                    RICHLABEL_COLOR_DEFAULT, "larger serif words",                     DEMO_NO_PARAGRAPH },
    +    { "DejaVu Sans", 12, 0,                    RICHLABEL_COLOR_DEFAULT, " and ",                                  DEMO_NO_PARAGRAPH },
    +    { "Cousine",     12, 0,                    DEMO_BLUE,               "code()",                                 DEMO_NO_PARAGRAPH },
    +    { "DejaVu Sans", 12, 0,                    RICHLABEL_COLOR_DEFAULT, ", wrapped to its width on every line.", FLOW_ALIGN_CENTER },
    +    { "Pacifico",    24, 0,                    RICHLABEL_COLOR_DEFAULT, "A centered paragraph",                   DEMO_NO_PARAGRAPH }
    +};
    +
    +// The Rich Text tab's buttons, each turning its style on and off on the
    +// selection.  No accelerators: B, I, and U are taken by the tabs and
    +// controls before them, and the box itself has Ctrl+B, I, and U.
    +typedef struct {
    +    const char *label;
    +    uint32_t    style;
    +} DemoStyleBtnT;
    +
    +static const DemoStyleBtnT sStyleBtns[] = {
    +    { "Bold",      FONT_STYLE_BOLD      },
    +    { "Italic",    FONT_STYLE_ITALIC    },
    +    { "Underline", FONT_STYLE_UNDERLINE }
    +};
    +
     // Last canvas point while drawing, so drags draw connected segments.
     static int32_t sCanvasLastX = 0;
     static int32_t sCanvasLastY = 0;
    @@ -211,6 +269,7 @@ static int32_t sCanvasLastY = 0;
     void        appShutdown(void);
     static void demoFilePath(char *buf, size_t bufSize, const char *name);
     static void onCanvasDraw(WidgetT *w, int32_t cx, int32_t cy, bool drag);
    +static void onChooseFont(WidgetT *w);
     static void onCloseCb(WindowT *win);
     static void onCloseMainCb(WindowT *win);
     static void onMenuCb(WindowT *win, int32_t menuId);
    @@ -218,6 +277,7 @@ static void onOkClick(WidgetT *w);
     static void onPaintColor(WindowT *win, RectT *dirtyArea);
     static void onPaintPattern(WindowT *win, RectT *dirtyArea);
     static void onPaintText(WindowT *win, RectT *dirtyArea);
    +static void onRichStyle(WidgetT *w);
     static void onScrollPattern(WindowT *win, ScrollbarOrientE orient, int32_t value);
     static void onToolbarClick(WidgetT *w);
     static void setStatusByName(WidgetT *from, const char *name, const char *text);
    @@ -283,6 +343,27 @@ static void onCanvasDraw(WidgetT *w, int32_t cx, int32_t cy, bool drag) {
     }
     
     
    +// Choose Font...: the font dialog, starting from the sample's font
    +static void onChooseFont(WidgetT *w) {
    +    WidgetT       *sample = wgtFind(w->window->widgetRoot, DEMO_FONT_SAMPLE);
    +    DvxFontChoiceT choice;
    +
    +    if (!sample) {
    +        return;
    +    }
    +
    +    const BitmapFontT *cur = wgtFont(sample);
    +
    +    snprintf(choice.family, sizeof(choice.family), "%s", dvxFontFamilyOf(cur));
    +    choice.size  = cur->pixelSize;
    +    choice.style = cur->style;
    +
    +    if (dvxFontDialog(sAc, NULL, 0, &choice)) {
    +        wgtSetFont(sample, choice.family, choice.size, choice.style);
    +    }
    +}
    +
    +
     static void onCloseCb(WindowT *win) {
         dvxDestroyWindow(sAc, win);
     }
    @@ -548,6 +629,18 @@ static void onPaintText(WindowT *win, RectT *dirtyArea) {
     }
     
     
    +// Bold, Italic, Underline: the button's style (its DemoStyleBtnT in
    +// userData) on or off on the rich edit box's selection
    +static void onRichStyle(WidgetT *w) {
    +    WidgetT *edit = wgtFind(w->window->widgetRoot, DEMO_RICH_EDIT);
    +    uint32_t bit  = ((const DemoStyleBtnT *)w->userData)->style;
    +
    +    if (edit) {
    +        wgtRichEditSetSelStyle(edit, (wgtRichEditGetSelStyle(edit) & bit) ? 0 : bit, bit);
    +    }
    +}
    +
    +
     // The pattern window's scrollbars pan the pattern; repaint at the new offset.
     static void onScrollPattern(WindowT *win, ScrollbarOrientE orient, int32_t value) {
         (void)orient;
    @@ -924,6 +1017,58 @@ static void setupControlsWindow(void) {
         wgtSetText(dText, "Read-only");
         wgtSetEnabled(dText, false);
     
    +    // --- Tab 10: Fonts ---
    +    // Labels each in a font of their own, a RichLabel mixing fonts,
    +    // styles, and colors (wrapping at a set width), and the font dialog
    +    // changing a sample's font.
    +    WidgetT *page10f = wgtTabPage(tabs, "F&onts");
    +    WidgetT *serif   = wgtLabel(page10f, "DejaVu Serif, 16 pixels");
    +    WidgetT *mono    = wgtLabel(page10f, "Cousine bold, 14 pixels");
    +    WidgetT *script  = wgtLabel(page10f, "Pacifico, 24 pixels");
    +    WidgetT *rich    = wgtRichLabel(page10f);
    +    WidgetT *fontRow = wgtHBox(page10f);
    +    WidgetT *choose  = wgtButton(fontRow, "Choose &Font...");
    +    WidgetT *sample  = wgtLabel(fontRow, "The quick brown fox");
    +
    +    wgtSetFont(serif, "DejaVu Serif", 16, 0);
    +    wgtSetFont(mono, "Cousine", 14, FONT_STYLE_BOLD);
    +    wgtSetFont(script, "Pacifico", 24, 0);
    +    rich->prefW     = wgtPixels(DEMO_RICH_W);
    +    choose->onClick = onChooseFont;
    +    wgtSetName(sample, DEMO_FONT_SAMPLE);
    +
    +    for (int32_t i = 0; i < DVX_ARRAY_LEN(sRichRuns); i++) {
    +        wgtRichLabelSetFont(rich, sRichRuns[i].family, sRichRuns[i].size);
    +        wgtRichLabelSetStyle(rich, sRichRuns[i].style);
    +        wgtRichLabelSetColor(rich, sRichRuns[i].color);
    +        wgtRichLabelAddText(rich, sRichRuns[i].text);
    +
    +        if (sRichRuns[i].paragraph != DEMO_NO_PARAGRAPH) {
    +            wgtRichLabelAddParagraph(rich, sRichRuns[i].paragraph);
    +        }
    +    }
    +
    +    // --- Tab 11: Rich Text ---
    +    // A RichEdit with buttons that turn the selection's styles on and off.
    +    WidgetT *page11r  = wgtTabPage(tabs, "Rich Te&xt");
    +    WidgetT *styleRow = wgtHBox(page11r);
    +    WidgetT *edit     = wgtRichEdit(page11r);
    +
    +    for (int32_t i = 0; i < DVX_ARRAY_LEN(sStyleBtns); i++) {
    +        WidgetT *btn = wgtButton(styleRow, sStyleBtns[i].label);
    +
    +        btn->userData = (void *)&sStyleBtns[i];
    +        btn->onClick  = onRichStyle;
    +    }
    +
    +    edit->weight = WGT_WEIGHT_FILL;
    +    wgtSetName(edit, DEMO_RICH_EDIT);
    +    wgtSetText(edit, "Type here.\nSelect some text, then click Bold, Italic, or Underline (or press Ctrl+B, Ctrl+I, or Ctrl+U).");
    +    wgtRichEditSetSelLength(edit, DEMO_EDIT_HEADING);
    +    wgtRichEditSetSelFont(edit, DEMO_EDIT_FAMILY, DEMO_EDIT_SIZE);
    +    wgtRichEditSetSelStyle(edit, FONT_STYLE_BOLD, FONT_STYLE_BOLD);
    +    wgtRichEditSetSelStart(edit, 0);
    +
         // Status bar at bottom (outside tabs)
         WidgetT *sb = wgtStatusBar(root);
         WidgetT *sbLabel = wgtLabel(sb, "Ready");
    diff --git a/src/apps/kpunch/dvxdemo/dvxdemo.dhs b/src/apps/kpunch/dvxdemo/dvxdemo.dhs
    index b75fc274..93263053 100644
    --- a/src/apps/kpunch/dvxdemo/dvxdemo.dhs
    +++ b/src/apps/kpunch/dvxdemo/dvxdemo.dhs
    @@ -39,15 +39,15 @@ DVX Demo shows off the kinds of windows and controls DVX offers. It opens severa
     .table
       Window             What it shows
       ------             -------------
    -  DVX Information    A window with a full menu bar, keyboard shortcuts and a right-click menu.
    +  DVX Information    A window with a full menu bar, keyboard shortcuts, and a right-click menu.
       Color Gradient     A window filled with a smooth color fade. Resize it to see the fade redrawn.
       Pattern            A checkerboard pattern with scroll bars. Use the scroll bars to move the pattern.
    -  Widget Demo        A small form with text boxes, check boxes, option buttons and lists.
    +  Widget Demo        A small form with text boxes, check boxes, option buttons, and lists.
       Advanced Widgets   Many more controls, arranged on tabs.
       ANSI Terminal      A text terminal showing colors and line-drawing characters. It starts minimized as an icon at the bottom of the screen; double-click the icon to open it.
     .endtable
     
    -Each window can be moved, resized, minimized and closed on its own. DVX Demo ends when you close its last window.
    +Each window can be moved, resized, minimized, and closed on its own. DVX Demo ends when you close its last window.
     
     .link dvxdemo.info The DVX Information window
     .link dvxdemo.widgets The Widget Demo and Advanced Widgets windows
    @@ -66,10 +66,10 @@ This window shows a short description of DVX and a menu bar with many kinds of m
     .table
       Menu     Commands
       ----     --------
    -  File     New, Open..., Save and Exit. Open... and Save show the real file dialog, then tell you which file you picked; no file is opened or saved.
    -  Edit     Cut, Copy and Paste show a message.
    -  View     Terminal opens another ANSI Terminal window and Controls opens another Advanced Widgets window. Toolbar and Status Bar show check marks, Small, Medium and Large show a round mark for the one chosen, and Zoom opens a smaller menu. Debug Layout draws outlines around every control in DVX so you can see how windows are laid out; choose it again to turn it off.
    -  Window   Cascade, Tile Grid, Tile Horizontal and Tile Vertical arrange all open windows.
    +  File     New, Open..., Save, and Exit. Open... and Save show the real file dialog, then tell you which file you picked; no file is opened or saved.
    +  Edit     Cut, Copy, and Paste show a message.
    +  View     Terminal opens another ANSI Terminal window and Controls opens another Advanced Widgets window. Toolbar and Status Bar show check marks, Small, Medium, and Large show a round mark for the one chosen, and Zoom opens a smaller menu. Debug Layout draws outlines around every control in DVX so you can see how windows are laid out; choose it again to turn it off.
    +  Window   Cascade, Tile Grid, Tile Horizontal, and Tile Vertical arrange all open windows.
       Help     Contents opens this help. About... shows a short message.
     .endtable
     
    @@ -88,7 +88,7 @@ While this window is active:
     
     .h2 Right-Click Menu
     
    -Right-click inside the window to see a short menu with Cut, Copy, Paste and Properties....
    +Right-click inside the window to see a short menu with Cut, Copy, Paste, and Properties....
     
     .h2 Closing
     
    @@ -108,7 +108,7 @@ Closing this window (or choosing File > Exit) asks "Close this window?" first. T
     A small form with the controls most programs use:
     
     .list
    -.item Text boxes for a name, a password (the letters are hidden) and a phone number (only digits fit in the pattern).
    +.item Text boxes for a name, a password (the letters are hidden), and a phone number (only digits fit in the pattern).
     .item Two check boxes and a group of three option buttons.
     .item Two lists. In the Items list you can drag an item up or down to move it, and right-click for a short menu. The Multi list lets you select several items with Ctrl and Shift.
     .item OK shows "Button clicked!" at the top of the window.
    @@ -123,7 +123,7 @@ The tabs across the top each show a group of controls. The bar at the bottom sho
     .table
       Tab        What it shows
       ---        -------------
    -  Controls   A drop-down list, a combo box you can type in, progress bars, a slider and a number box.
    +  Controls   A drop-down list, a combo box you can type in, progress bars, a slider, and a number box.
       Tree       A tree of folders and files. Items can be dragged to a new place.
       List       A list with columns. Click a column heading to sort by it, and drag items to reorder them.
       Scroll     A scrolling area holding more controls than fit.
    @@ -132,4 +132,6 @@ The tabs across the top each show a group of controls. The bar at the bottom sho
       Editor     A multi-line text box, and a drawing area you can draw on with the mouse.
       Split      Areas divided by bars you can drag to change their sizes.
       Disabled   The same controls shown usable and grayed out, side by side.
    +  Fonts      Text in different fonts, a paragraph mixing fonts, styles, and colors, and a button that opens the Font dialog.
    +  Rich Text  A text box where words can be bold, italic, or underlined. Select some text, then click a button or press Ctrl+B, Ctrl+I, or Ctrl+U.
     .endtable
    diff --git a/src/apps/kpunch/dvxdemo/dvxdemo.res b/src/apps/kpunch/dvxdemo/dvxdemo.res
    index 7accf27a..dc033e3b 100644
    --- a/src/apps/kpunch/dvxdemo/dvxdemo.res
    +++ b/src/apps/kpunch/dvxdemo/dvxdemo.res
    @@ -5,3 +5,5 @@ author      text  "Scott Duensing"
     copyright   text  "Copyright 2026 Scott Duensing"
     publisher   text  "Kangaroo Punch Studios"
     description text  "Widget toolkit demonstration"
    +# The demo's own Pacifico (fonts.ini, made at build time)
    +pacifico    font  ../../../../obj/apps/dvxdemo/fonts/DEMOPAC.DFN
    diff --git a/src/apps/kpunch/dvxdemo/fonts.ini b/src/apps/kpunch/dvxdemo/fonts.ini
    new file mode 100644
    index 00000000..4d27b307
    --- /dev/null
    +++ b/src/apps/kpunch/dvxdemo/fonts.ini
    @@ -0,0 +1,20 @@
    +; fonts.ini -- the font DVX Demo carries (made by mkfont at build time;
    +; dvxdemo.res makes it a font resource).  Pacifico at the one size the
    +; demo draws it, smoothed too, with only the characters it shows: the
    +; demo's Pacifico is its own, so it shows where the Pacifico family is
    +; not installed (Setup's base set of fonts), and where it is, this size
    +; comes from here and the installed family fills in the rest.
    +
    +[fonts]
    +DEMOPAC = Pacifico
    +
    +[licences]
    +PACIFICO = pacifico/OFL.txt
    +
    +[DEMOPAC]
    +licence  = PACIFICO
    +source   = pacifico/Pacifico-Regular.ttf
    +sizes    = 24
    +smooth   = 24
    +hinting  = auto
    +chars    = 20-7E
    diff --git a/src/apps/kpunch/dvxhelp/help.dhs b/src/apps/kpunch/dvxhelp/help.dhs
    index 7b1673c9..d161e255 100644
    --- a/src/apps/kpunch/dvxhelp/help.dhs
    +++ b/src/apps/kpunch/dvxhelp/help.dhs
    @@ -32,7 +32,7 @@
     DVX Help shows help pages. The list on the left is the table of contents, the page you are reading is on the right, and underlined words are links to other pages. You can also look things up in the Index or with Search.
     
     .note tip
    -New to DVX? Click the Program Manager window and press F1, or choose DVX Help from the Program Manager's Help menu, to open the DVX user guide. It explains how to start programs, work with windows, menus and dialog boxes, and everything else that works the same in every program. Each program's own commands are explained in that program's help: press F1 while you work in it.
    +New to DVX? Click the Program Manager window and press F1, or choose DVX Help from the Program Manager's Help menu, to open the DVX user guide. It explains how to start programs, work with windows, menus, and dialog boxes, and everything else that works the same in every program. Each program's own commands are explained in that program's help: press F1 while you work in it.
     .endnote
     
     .h2 Opening Help
    @@ -98,7 +98,7 @@ Help files are authored as plain text .dhs source files using a simple line-orie
       .link      Hyperlink to another topic in the same help
                            file (see Links below)
       .image  [left|center|right]
    -                       Inline image (BMP, PNG, JPEG or GIF), path
    +                       Inline image (BMP, PNG, JPEG, or GIF), path
                            relative to the -i directory
     .endtable
     
    @@ -215,7 +215,7 @@ Multiple input files are merged into a single help file. This allows per-widget
     
     .h2 Input Files
     
    -Each input file starts fresh: no topic is current and no .list, .note, .code or .table block is left open from the previous file. Text before a file's first .topic is ignored, and directives there are errors. A .code or .table block still open at the end of a file is closed with a warning. Files named with .include are different: they are read in place, as if their lines were part of the including file.
    +Each input file starts fresh: no topic is current and no .list, .note, .code, or .table block is left open from the previous file. Text before a file's first .topic is ignored, and directives there are errors. A .code or .table block still open at the end of a file is closed with a warning. Files named with .include are different: they are read in place, as if their lines were part of the including file.
     
     Source lines longer than 4094 characters, and lines that contain a NUL byte, are errors. The .image directive may name a path relative to the -i directory, including a subdirectory (for example sub/pic.png).
     
    @@ -223,7 +223,7 @@ If any error is reported the compile stops and the previous output file is kept.
     
     .h2 Links
     
    -Every .link must name a topic that is compiled into the same help file: a topic in any of the input files or the files they include, before or after the link. A link to any other ID is an error that names the file, the line and the missing ID. The one exception is the help DVX rebuilds by itself when it starts after programs, widgets or libraries were added or removed: there a link to a topic that is no longer present shows as plain text instead, so removing a piece of DVX never breaks the rest of the help. Links cannot reach into another help file; name the other file's page in the text instead. A .link line inside a .code block is example text and is never checked.
    +Every .link must name a topic that is compiled into the same help file: a topic in any of the input files or the files they include, before or after the link. A link to any other ID is an error that names the file, the line, and the missing ID. The one exception is the help DVX rebuilds by itself when it starts after programs, widgets, or libraries were added or removed: there a link to a topic that is no longer present shows as plain text instead, so removing a piece of DVX never breaks the rest of the help. Links cannot reach into another help file; name the other file's page in the text instead. A .link line inside a .code block is example text and is never checked.
     
     .h2 Build Integration
     
    diff --git a/src/apps/kpunch/dvxhelp/helpedit/helpedit.dhs b/src/apps/kpunch/dvxhelp/helpedit/helpedit.dhs
    index 2137ec60..4fcc9368 100644
    --- a/src/apps/kpunch/dvxhelp/helpedit/helpedit.dhs
    +++ b/src/apps/kpunch/dvxhelp/helpedit/helpedit.dhs
    @@ -35,7 +35,7 @@
     
     .h1 Help Editor
     
    -The Help Editor is for writing help files, like the one you are reading now. You type the help text with a few simple commands that mark titles, headings, lists and links, then turn it into a finished help file and look at it in DVX Help.
    +The Help Editor is for writing help files, like the one you are reading now. You type the help text with a few simple commands that mark titles, headings, lists, and links, then turn it into a finished help file and look at it in DVX Help.
     
     Help text is kept in a help source file, whose name ends in .dhs. The finished help file has the same name, ending in .hlp, and is saved in the same folder.
     
    @@ -69,9 +69,9 @@ Double-clicking a help source in the File Manager (or starting one from the Prog
     
     .h1 Writing Help Text
     
    -When the Help Editor starts, and after File > New, the page holds a small example: one topic with a title, a table of contents entry and a heading. Replace it with your own text.
    +When the Help Editor starts, and after File > New, the page holds a small example: one topic with a title, a table of contents entry, and a heading. Replace it with your own text.
     
    -A help file is made of topics, the pages you see in DVX Help. Lines that start with a period are commands, for example .topic starts a new topic, .title gives it a title and .h1 makes a heading. Every other line is ordinary text that DVX Help wraps to fit its window. Leave an empty line between paragraphs.
    +A help file is made of topics, the pages you see in DVX Help. Lines that start with a period are commands, for example .topic starts a new topic, .title gives it a title, and .h1 makes a heading. Every other line is ordinary text that DVX Help wraps to fit its window. Leave an empty line between paragraphs.
     
     Choose Help > Help File Format to see every command, with an example.
     
    @@ -82,7 +82,7 @@ Choose Help > Help File Format to see every command, with an example.
     .item Line numbers are shown down the left side. They help you find your place in a long file.
     .item Pressing Enter starts the new line with the same indent as the line above.
     .item The Tab key puts a tab in your text.
    -.item The usual editing keys work, including Ctrl+X, Ctrl+C and Ctrl+V to cut, copy and paste, and Ctrl+Z to undo.
    +.item The usual editing keys work, including Ctrl+X, Ctrl+C, and Ctrl+V to cut, copy, and paste, and Ctrl+Z to undo.
     .endlist
     
     A star (*) at the start of the title bar means there are unsaved changes. The Help Editor asks "Save changes?" before it starts a new file, opens another one, or closes.
    diff --git a/src/apps/kpunch/fileMgr/fileMgr.dbp b/src/apps/kpunch/fileMgr/fileMgr.dbp
    index 9272e53e..ddb897c1 100644
    --- a/src/apps/kpunch/fileMgr/fileMgr.dbp
    +++ b/src/apps/kpunch/fileMgr/fileMgr.dbp
    @@ -4,7 +4,7 @@ Author = Scott Duensing
     Publisher = Kangaroo Punch Studios
     Version = 1.00
     Copyright = Copyright 2026 Scott Duensing
    -Description = Browse, copy, move, rename and delete files and folders
    +Description = Browse, copy, move, rename, and delete files and folders
     Icon = ICON32.BMP
     
     [Modules]
    diff --git a/src/apps/kpunch/fileMgr/fileMgr.dhs b/src/apps/kpunch/fileMgr/fileMgr.dhs
    index 4869cc8f..75635685 100644
    --- a/src/apps/kpunch/fileMgr/fileMgr.dhs
    +++ b/src/apps/kpunch/fileMgr/fileMgr.dhs
    @@ -33,19 +33,19 @@
     
     .h1 File Manager
     
    -The File Manager shows the files and folders on your disks and lets you open, copy, move, rename and delete them.
    +The File Manager shows the files and folders on your disks and lets you open, copy, move, rename, and delete them.
     
     .link filemgr.window The parts of the window
     .link filemgr.open Opening files and starting programs
     .link filemgr.select Choosing files
     .link filemgr.copy Copying and moving
    -.link filemgr.organize New folders, renaming and deleting
    +.link filemgr.organize New folders, renaming, and deleting
     .link filemgr.props Properties
     .link filemgr.keys Keys and menus
     
     .h2 More Than One Window
     
    -You can open the File Manager more than once, for example one window on each of two folders. File > New Window opens another File Manager on the folder you are looking at. Copy, Cut and Paste move files between the windows (see Copying and Moving).
    +You can open the File Manager more than once, for example one window on each of two folders. File > New Window opens another File Manager on the folder you are looking at. Copy, Cut, and Paste move files between the windows (see Copying and Moving).
     
     .topic filemgr.window
     .title The File Manager Window
    @@ -83,13 +83,13 @@ The right side lists the folder's contents: folders first, then files. The colum
       Modified   The date and time the file was last changed.
     .endtable
     
    -Click a column heading to sort by that column; click it again to reverse the order. The View menu has the same choices (Sort by Name, Size, Type or Date, and Descending). Folders always stay at the top.
    +Click a column heading to sort by that column; click it again to reverse the order. The View menu has the same choices (Sort by Name, Size, Type, or Date, and Descending). Folders always stay at the top.
     
     To show only some files, choose View > Filter... and type a pattern such as *.TXT. Several patterns can be separated with semicolons, for example *.TXT;*.BAT. *.* shows everything again. While a filter is on, it is shown at the end of the window title.
     
     .h2 Status Bar
     
    -The bottom line shows how many items are in the folder, how many you have selected and how big they are together, and how much free space is left on the drive.
    +The bottom line shows how many items are in the folder, how many you have selected, and how big they are together, and how much free space is left on the drive.
     
     .topic filemgr.open
     .title Opening Files and Starting Programs
    @@ -129,7 +129,7 @@ Most commands work on the selected files and folders in the list.
     .item Edit > Select All (Ctrl+A) selects everything; Edit > Invert Selection selects everything that was not selected and unselects the rest.
     .endlist
     
    -If nothing is selected, commands work on the highlighted item. When the folder tree has the focus (click in it, or press Tab), Rename, Delete, Copy, Cut and Properties work on the folder chosen in the tree instead.
    +If nothing is selected, commands work on the highlighted item. When the folder tree has the focus (click in it, or press Tab), Rename, Delete, Copy, Cut, and Properties work on the folder chosen in the tree instead.
     
     .topic filemgr.copy
     .title Copying and Moving
    @@ -152,7 +152,7 @@ Folders are copied or moved with everything in them. Copies keep the date of the
     
     .h2 While It Works
     
    -Copying, moving and deleting happen in the background, in a small window that shows what is being done and how far along it is. You can keep working in DVX meanwhile. Click Cancel (or close that window) to stop; a file that was only partly copied is removed, and everything finished before that stays done.
    +Copying, moving, and deleting happen in the background, in a small window that shows what is being done and how far along it is. You can keep working in DVX meanwhile. Click Cancel (or close that window) to stop; a file that was only partly copied is removed, and everything finished before that stays done.
     
     .h2 When a File Already Exists
     
    @@ -169,12 +169,12 @@ If the place you copy or move to already has a file with the same name, you are
     
     .h2 When Something Goes Wrong
     
    -If a file cannot be read or written (for example the disk is full, write-protected, or was taken out of the drive), you are told why and can choose Retry (try again, for example after putting the disk back), Skip (leave this item out and go on) or Cancel. A folder whose contents were skipped is not removed by a move.
    +If a file cannot be read or written (for example the disk is full, write-protected, or was taken out of the drive), you are told why and can choose Retry (try again, for example after putting the disk back), Skip (leave this item out and go on), or Cancel. A folder whose contents were skipped is not removed by a move.
     
     A folder cannot be copied or moved into itself or into one of its own folders, and a file cannot be copied onto itself; you are told if you try.
     
     .topic filemgr.organize
    -.title New Folders, Renaming and Deleting
    +.title New Folders, Renaming, and Deleting
     .toc 1 Organizing
     .index New Folder
     .index Renaming Files
    @@ -182,7 +182,7 @@ A folder cannot be copied or moved into itself or into one of its own folders, a
     .index Read-Only Files
     .index File Names
     
    -.h1 New Folders, Renaming and Deleting
    +.h1 New Folders, Renaming, and Deleting
     
     .h2 New Folder
     
    @@ -194,11 +194,11 @@ Select one file or folder and choose File > Rename... or press F2, then type the
     
     .h2 Delete
     
    -Select the files and folders and press Del or choose File > Delete. You are asked to confirm first. Folders are deleted with everything in them. If a file is read-only, you are asked again before it is deleted (Yes, No, Yes to All or Cancel).
    +Select the files and folders and press Del or choose File > Delete. You are asked to confirm first. Folders are deleted with everything in them. If a file is read-only, you are asked again before it is deleted (Yes, No, Yes to All, or Cancel).
     
     .h2 File Names
     
    -What a name may be depends on the disk. When the disk takes long file names (DVX running under Windows, or a long file name driver such as DOSLFN loaded before DVX), a name can have up to 255 characters, including spaces, several dots and + , ; = [ ] (for example Letter to Mom.final.txt); it cannot end with a dot or a space. Otherwise names follow the DOS rules: up to eight characters, optionally followed by a dot and up to three more (for example REPORT.TXT), and spaces and + , ; = [ ] are not allowed. On every disk the characters " * / : < > ? \ | and device names such as CON, PRN or LPT1 are not allowed. If a name is not allowed, the File Manager tells you why.
    +What a name may be depends on the disk. When the disk takes long file names (DVX running under Windows, or a long file name driver such as DOSLFN loaded before DVX), a name can have up to 255 characters, including spaces, several dots, and + , ; = [ ] (for example Letter to Mom.final.txt); it cannot end with a dot or a space. Otherwise names follow the DOS rules: up to eight characters, optionally followed by a dot and up to three more (for example REPORT.TXT), and spaces and + , ; = [ ] are not allowed. On every disk the characters " * / : < > ? \ | and device names such as CON, PRN, or LPT1 are not allowed. If a name is not allowed, the File Manager tells you why.
     
     A new folder is first named New Folder on a disk that takes long names, NEWFOLDR on one that does not; type over it.
     
    @@ -239,7 +239,7 @@ The Attributes boxes can be changed; click OK to apply them to every selected it
     .table
       Key            What it does
       ---            ------------
    -  Tab            Move between the drive bar, the path bar, the folder tree and the file list.
    +  Tab            Move between the drive bar, the path bar, the folder tree, and the file list.
       Enter          Open the highlighted item.
       Backspace      Go up to the parent folder.
       Del            Delete the selection.
    @@ -266,7 +266,7 @@ The Attributes boxes can be changed; click OK to apply them to every selected it
       File    Move To             Move the selection to a folder you type.
       File    Rename              Rename the selected item.
       File    Delete              Delete the selection.
    -  File    Properties          Size, date and attributes.
    +  File    Properties          Size, date, and attributes.
       File    New Window          Open another File Manager window.
       File    Exit                Close this window.
       Edit    Cut, Copy, Paste    Move or copy files between folders and windows.
    @@ -275,10 +275,10 @@ The Attributes boxes can be changed; click OK to apply them to every selected it
       View    Refresh             Read the folder again.
       View    Filter              Show only files matching a pattern.
       View    Up One Level        Go to the parent folder.
    -  View    Sort by ...         Sort by name, size, type or date.
    +  View    Sort by ...         Sort by name, size, type, or date.
       View    Descending          Reverse the sort order.
       Help    Contents            Open this help.
       Help    About               Show the program name and copyright.
     .endtable
     
    -If you close a File Manager window while it is still copying, moving or deleting, you are asked whether to stop.
    +If you close a File Manager window while it is still copying, moving, or deleting, you are asked whether to stop.
    diff --git a/src/apps/kpunch/fileMgr/fileMgr.frm b/src/apps/kpunch/fileMgr/fileMgr.frm
    index 9ce23837..d94fbd51 100644
    --- a/src/apps/kpunch/fileMgr/fileMgr.frm
    +++ b/src/apps/kpunch/fileMgr/fileMgr.frm
    @@ -1513,7 +1513,7 @@ SUB mnuAbout_Click
         DIM msg AS STRING
     
         msg = "File Manager" & CHR$(10) & CHR$(10)
    -    msg = msg & "Browse, copy, move, rename and delete files and folders." & CHR$(10) & CHR$(10)
    +    msg = msg & "Browse, copy, move, rename, and delete files and folders." & CHR$(10) & CHR$(10)
         msg = msg & "Copyright 2026 Scott Duensing"
         MsgBox msg, vbOKOnly + vbInformation, "About File Manager"
     END SUB
    diff --git a/src/apps/kpunch/iconed/iconed.dhs b/src/apps/kpunch/iconed/iconed.dhs
    index d7a59f89..7f97d3fb 100644
    --- a/src/apps/kpunch/iconed/iconed.dhs
    +++ b/src/apps/kpunch/iconed/iconed.dhs
    @@ -90,7 +90,7 @@ There is no Undo. To fix a mistake, paint over it.
     
     .h2 Picture Size
     
    -The Size menu chooses how many dots the picture has: 16 x 16, 24 x 24 or 32 x 32. New pictures start at 32 x 32. Changing the size starts a fresh, empty picture; if you have unsaved changes, the Icon Editor asks first.
    +The Size menu chooses how many dots the picture has: 16 x 16, 24 x 24, or 32 x 32. New pictures start at 32 x 32. Changing the size starts a fresh, empty picture; if you have unsaved changes, the Icon Editor asks first.
     
     .topic iconed.files
     .title Opening and Saving Pictures
    @@ -115,7 +115,7 @@ The Size menu chooses how many dots the picture has: 16 x 16, 24 x 24 or 32 x 32
     
     .h2 Opening
     
    -The Icon Editor can only open square pictures that are 16 x 16, 24 x 24 or 32 x 32 dots. Anything else is refused with a message, and your current picture stays as it was. Opening a picture switches the Size menu to match it.
    +The Icon Editor can only open square pictures that are 16 x 16, 24 x 24, or 32 x 32 dots. Anything else is refused with a message, and your current picture stays as it was. Opening a picture switches the Size menu to match it.
     
     A picture named when the Icon Editor starts, for example in the Arguments of a Program Manager item for it, opens right away.
     
    diff --git a/src/apps/kpunch/iconed/iconed.frm b/src/apps/kpunch/iconed/iconed.frm
    index 3fe59a61..c1888652 100644
    --- a/src/apps/kpunch/iconed/iconed.frm
    +++ b/src/apps/kpunch/iconed/iconed.frm
    @@ -482,7 +482,7 @@ SUB OpenIcon(path AS STRING)
             ' Put the current icon back into the preview.
             cvPreview.Resize iconW, iconH
             DrawPreview
    -        MsgBox "Icons must be 16 x 16, 24 x 24 or 32 x 32 pixels."
    +        MsgBox "Icons must be 16 x 16, 24 x 24, or 32 x 32 pixels."
             EXIT SUB
         END IF
         SetIconSize w
    diff --git a/src/apps/kpunch/imgview/imgview.dhs b/src/apps/kpunch/imgview/imgview.dhs
    index 678041ae..c38415f8 100644
    --- a/src/apps/kpunch/imgview/imgview.dhs
    +++ b/src/apps/kpunch/imgview/imgview.dhs
    @@ -37,7 +37,7 @@
     
     .h1 Image Viewer
     
    -The Image Viewer shows pictures. It can open pictures saved in the BMP, PNG, JPEG and GIF formats.
    +The Image Viewer shows pictures. It can open pictures saved in the BMP, PNG, JPEG, and GIF formats.
     
     .h2 Opening a Picture
     
    diff --git a/src/apps/kpunch/imgview/imgview.frm b/src/apps/kpunch/imgview/imgview.frm
    index f4c3370a..71fdaea3 100644
    --- a/src/apps/kpunch/imgview/imgview.frm
    +++ b/src/apps/kpunch/imgview/imgview.frm
    @@ -131,7 +131,7 @@ END SUB
     SUB mnuAbout_Click
         DIM msg AS STRING
         msg = "Image Viewer" + CHR$(10) + CHR$(10)
    -    msg = msg + "Shows BMP, PNG, JPEG and GIF pictures." + CHR$(10) + CHR$(10)
    +    msg = msg + "Shows BMP, PNG, JPEG, and GIF pictures." + CHR$(10) + CHR$(10)
         msg = msg + "Copyright 2026 Scott Duensing"
         MsgBox msg, vbOKOnly + vbInformation, "About Image Viewer"
     END SUB
    diff --git a/src/apps/kpunch/notepad/notepad.dhs b/src/apps/kpunch/notepad/notepad.dhs
    index 400f7e1f..af4e778f 100644
    --- a/src/apps/kpunch/notepad/notepad.dhs
    +++ b/src/apps/kpunch/notepad/notepad.dhs
    @@ -33,7 +33,7 @@
     
     .h1 Notepad
     
    -Notepad is a simple text editor. Use it for notes, lists, letters and small text files such as settings files and batch files.
    +Notepad is a simple text editor. Use it for notes, lists, letters, and small text files such as settings files and batch files.
     
     The Notepad window has a menu bar at the top and a large typing area below it. Click in the typing area and start typing. The title bar shows the name of the file you are working on, or Untitled for a new page.
     
    @@ -46,7 +46,7 @@ The Notepad window has a menu bar at the top and a large typing area below it. C
       File    Open...        Open a text file.
       File    Save           Save your text to its file (asks for a name the first time).
       File    Save As...     Save your text under a new name.
    -  File    Page Setup...  Choose the margins, header, footer and other page settings for printing.
    +  File    Page Setup...  Choose the margins, header, footer, and other page settings for printing.
       File    Print...       Print your text.
       File    Exit           Close Notepad.
       Edit    Cut            Move the selected text to the clipboard.
    @@ -136,7 +136,7 @@ The scroll bars and the mouse wheel move through long text.
     
     Hold Shift while you use any of the keys above, or drag across the text with the mouse. Double-click selects a word and a triple-click selects a whole line. Ctrl+A (or Edit > Select All) selects everything.
     
    -.h2 Cut, Copy, Paste and Undo
    +.h2 Cut, Copy, Paste, and Undo
     
     .table
       Key       What it does
    @@ -162,7 +162,7 @@ The clipboard is shared by all programs, so you can copy text in Notepad and pas
     
     Choose File > Print... to print your text. The Print dialog lets you choose where the printout goes (a printer port, or a file), how many copies you want, and whether to print every page or only some of them. Choose Print to start. While it prints you can keep working in other programs; the Printing window has a Cancel button if you change your mind.
     
    -If the printer runs out of paper, is switched off or does not answer, Notepad tells you. Fix the problem and choose Retry, or choose Cancel to stop printing.
    +If the printer runs out of paper, is switched off, or does not answer, Notepad tells you. Fix the problem and choose Retry, or choose Cancel to stop printing.
     
     .h2 Page Setup
     
    @@ -179,6 +179,6 @@ Choose File > Page Setup... to change how your text is laid out on paper. The se
       Wrap long lines       On: a line too long for the page continues on the next line. Off: the end of the line is left off.
     .endtable
     
    -In the header and footer, &f prints the file name, &p the page number, &n the number of pages, &d the date and &t the time. &l, &c and &r put what follows at the left, in the centre or at the right. For example, "&l&f&rPage &p" puts the file name on the left and the page number on the right.
    +In the header and footer, &f prints the file name, &p the page number, &n the number of pages, &d the date, and &t the time. &l, &c, and &r put what follows at the left, in the centre, or at the right. For example, "&l&f&rPage &p" puts the file name on the left and the page number on the right.
     
     Which printer you have and where it is connected is set on the Control Panel's Printer page.
    diff --git a/src/apps/kpunch/progman/pmProps.c b/src/apps/kpunch/progman/pmProps.c
    index 8a0c28cc..20b461cc 100644
    --- a/src/apps/kpunch/progman/pmProps.c
    +++ b/src/apps/kpunch/progman/pmProps.c
    @@ -144,7 +144,7 @@ static bool onOk(WindowT *dlg, void *userCtx) {
         }
     
         if (!pmHotkeyParse(wgtGetText(pd->hotkey), &key, &mods)) {
    -        dvxErrorBox(pd->ctx, "Properties", "A shortcut key is Ctrl with Alt or Shift, and a letter, digit or F1 to F12, for example Ctrl+Alt+P.");
    +        dvxErrorBox(pd->ctx, "Properties", "A shortcut key is Ctrl with Alt or Shift, and a letter, digit, or F1 to F12, for example Ctrl+Alt+P.");
             return false;
         }
     
    diff --git a/src/apps/kpunch/progman/progman.dhs b/src/apps/kpunch/progman/progman.dhs
    index 3a1444dc..3a5d3ebe 100644
    --- a/src/apps/kpunch/progman/progman.dhs
    +++ b/src/apps/kpunch/progman/progman.dhs
    @@ -135,12 +135,12 @@ Right-click an icon for the same commands.
       Program           The program to start. Click Browse... to pick it. It can also be a document, such as a text file: it then opens in the program set up for its type (see Running a Command).
       Arguments         Extra information given to the program when it starts, such as the name of a file to open.
       Working folder    The folder where files named in Arguments are looked for. For example, with the working folder LETTERS and the argument JUNE.TXT, the program gets LETTERS\JUNE.TXT.
    -  Icon file         A different picture for the icon: a program file (its icon is used) or a picture (BMP, PNG, JPEG or GIF). Leave it empty for the program's own icon. The small picture beside Run minimized shows the icon.
    -  Shortcut key      Keys that start the item from any program, whichever window is in front, such as Ctrl+Alt+N. They work while the Program Manager is running (it may be minimized), but not while a menu or a dialog is open. A shortcut is Ctrl together with Alt or Shift (or both), plus a letter, a digit or F1 to F12. Each shortcut can belong to one item only.
    +  Icon file         A different picture for the icon: a program file (its icon is used) or a picture (BMP, PNG, JPEG, or GIF). Leave it empty for the program's own icon. The small picture beside Run minimized shows the icon.
    +  Shortcut key      Keys that start the item from any program, whichever window is in front, such as Ctrl+Alt+N. They work while the Program Manager is running (it may be minimized), but not while a menu or a dialog is open. A shortcut is Ctrl together with Alt or Shift (or both), plus a letter, a digit, or F1 to F12. Each shortcut can belong to one item only.
       Run minimized     Start the program with its windows made into icons.
     .endtable
     
    -Items in Installed Programs have no properties to change; Properties just shows the program's name, file and description.
    +Items in Installed Programs have no properties to change; Properties just shows the program's name, file, and description.
     
     .topic progman.arrange
     .title Views and Arranging Icons
    @@ -173,9 +173,9 @@ Installed Programs is always in alphabetical order. In the List view, dragging c
     
     .h2 Searching
     
    -Type in the Search box to show only the programs, in every group, whose name, description or file name contains what you typed. Clear the box, or click a group, to go back.
    +Type in the Search box to show only the programs, in every group, whose name, description, or file name contains what you typed. Clear the box, or click a group, to go back.
     
    -Your groups, items, their places, the view and the options are all saved as you change them, and are there again the next time you start DVX.
    +Your groups, items, their places, the view, and the options are all saved as you change them, and are there again the next time you start DVX.
     
     .topic progman.run
     .title Running a Command
    @@ -205,7 +205,7 @@ You can also type (or put in an item's Program box) a document instead of a prog
     
     .h1 Keyboard Shortcuts
     
    -Tab moves between the group list, the Search box and the icons.
    +Tab moves between the group list, the Search box, and the icons.
     
     .table
       Key                          What it does
    diff --git a/src/apps/kpunch/progman/userHelp.dhs b/src/apps/kpunch/progman/userHelp.dhs
    index fdd086da..fad330b3 100644
    --- a/src/apps/kpunch/progman/userHelp.dhs
    +++ b/src/apps/kpunch/progman/userHelp.dhs
    @@ -43,13 +43,13 @@
     
     .image help.png center
     
    -DVX is a graphical desktop for DOS computers. Instead of typing commands at a prompt, you work with windows, menus, icons and a mouse, much like Windows 3.1.
    +DVX is a graphical desktop for DOS computers. Instead of typing commands at a prompt, you work with windows, menus, icons, and a mouse, much like Windows 3.1.
     
     With DVX you can run several programs at the same time, each in its own window. You can move and resize those windows, switch between them, and copy text from one program to another.
     
     .h2 About This Guide
     
    -This guide explains the things that work the same way everywhere in DVX: starting programs, using windows, menus and dialog boxes, opening and saving files, and getting out of trouble.
    +This guide explains the things that work the same way everywhere in DVX: starting programs, using windows, menus, and dialog boxes, opening and saving files, and getting out of trouble.
     
     Every program also has its own help, which explains that program's commands. Open a program and press F1, or choose Contents from its Help menu.
     
    @@ -89,7 +89,7 @@ Press F1 at any time to open help. Use the list on the left side of the help win
     
     .h2 Starting DVX
     
    -At the DOS prompt, change to the folder where DVX is installed, type DVX and press Enter. You can also start DVX from any folder by typing its full path, for example C:\DVX\DVX.
    +At the DOS prompt, change to the folder where DVX is installed, type DVX, and press Enter. You can also start DVX from any folder by typing its full path, for example C:\DVX\DVX.
     
     While DVX loads you see a title screen with a bar that fills from left to right. Sometimes the bar fills a second time. That happens after programs or parts of DVX were added or removed, while DVX brings its help files up to date. It only takes a moment.
     
    @@ -152,7 +152,7 @@ Moving the mouse moves the pointer on the screen. These are the basic mouse acti
     
     If you hold the pointer still over some buttons and icons, a small tip appears that tells you what they do.
     
    -The speed of the pointer, the double-click speed and the scroll wheel can be changed. See Changing Settings.
    +The speed of the pointer, the double-click speed, and the scroll wheel can be changed. See Changing Settings.
     
     .h2 The Scroll Wheel
     
    @@ -163,14 +163,14 @@ If your mouse has a scroll wheel, turning it scrolls the window you are working
     Most things you can do with the mouse you can also do with the keyboard:
     
     .list
    -.item Tab moves to the next button, box or list in a window. Shift+Tab moves back.
    +.item Tab moves to the next button, box, or list in a window. Shift+Tab moves back.
     .item Enter or the Space bar presses the button that is highlighted.
     .item F10 opens the menu bar of the active window. Alt plus an underlined letter opens that menu directly.
     .item Alt+Tab switches to the next window.
     .item F1 opens help.
     .endlist
     
    -Letters that are underlined in menus, buttons and labels are shortcuts. Hold Alt and press the letter to choose that item.
    +Letters that are underlined in menus, buttons, and labels are shortcuts. Hold Alt and press the letter to choose that item.
     
     .link user.keys See the full list of keyboard shortcuts
     .link user.settings Changing Settings
    @@ -292,7 +292,7 @@ Press Ctrl+F12 to save a picture of the whole screen, or Ctrl+Shift+F12 to save
     
     .h2 Using Menus
     
    -Many windows have a menu bar under the title bar, with words such as File, Edit and Help. Each word opens a menu of commands.
    +Many windows have a menu bar under the title bar, with words such as File, Edit, and Help. Each word opens a menu of commands.
     
     With the mouse, click a menu name to open it, then click a command. While a menu is open, move the pointer along the menu bar to look at the other menus. Click anywhere outside the menu to close it without choosing anything.
     
    @@ -324,8 +324,8 @@ Many programs use the same menu names for the same kinds of commands:
     .table
       Menu   Usual commands
       ----   --------------
    -  File   New, Open..., Save, Save As..., Page Setup..., Print... and Exit.
    -  Edit   Cut, Copy, Paste and Select All.
    +  File   New, Open..., Save, Save As..., Page Setup..., Print..., and Exit.
    +  Edit   Cut, Copy, Paste, and Select All.
       Help   Contents opens the program's own help. About shows the program's name and copyright.
     .endtable
     
    @@ -334,7 +334,7 @@ Many programs use the same menu names for the same kinds of commands:
     A dialog box is a small window that asks you a question or asks for details. While a dialog box is open you must answer it before you can use the rest of the program.
     
     .list
    -.item Enter presses the main button, usually OK, Open, Save or Yes.
    +.item Enter presses the main button, usually OK, Open, Save, or Yes.
     .item Esc presses Cancel (or No), and closes the box without making changes.
     .item Closing a dialog box with its Close box counts as Cancel.
     .item Tab moves between the parts of the dialog box.
    @@ -352,12 +352,12 @@ When you close a program, or start a new file, while you have unsaved work, most
     .item Cancel goes back to your work without doing anything.
     .endlist
     
    -.link user.controls Next: Buttons, Lists and Other Controls
    +.link user.controls Next: Buttons, Lists, and Other Controls
     .link user.filedlg Opening and Saving Files
     
     .topic user.controls
    -.title Buttons, Lists and Other Controls
    -.toc 1 Buttons, Lists and Other Controls
    +.title Buttons, Lists, and Other Controls
    +.toc 1 Buttons, Lists, and Other Controls
     .index Controls
     .index Buttons
     .index Check Boxes
    @@ -371,7 +371,7 @@ When you close a program, or start a new file, while you have unsaved work, most
     .index Tabs
     .index Undo
     
    -.h1 Buttons, Lists and Other Controls
    +.h1 Buttons, Lists, and Other Controls
     
     Windows and dialog boxes are built from a small set of controls. They all work the same way in every program.
     
    @@ -389,7 +389,7 @@ Option buttons come in groups, and only one in a group can be chosen. Click the
     
     .h2 Lists
     
    -A list shows several items. Click an item to select it, and double-click it (or press Enter) to open or use it. Up, Down, Page Up, Page Down, Home and End move through the list. Typing a letter jumps to the next item that starts with that letter.
    +A list shows several items. Click an item to select it, and double-click it (or press Enter) to open or use it. Up, Down, Page Up, Page Down, Home, and End move through the list. Typing a letter jumps to the next item that starts with that letter.
     
     Some lists let you select more than one item. Hold Ctrl while clicking to add or remove single items, or hold Shift while clicking to select everything between two items.
     
    @@ -397,7 +397,7 @@ Some lists have column headings. Click a heading to sort the list by that column
     
     .h2 Drop-down Lists
     
    -A drop-down list shows one choice with an arrow button beside it. Click it to see all the choices, then click the one you want. With the keyboard, Up and Down change the choice, and Enter, the Space bar, F4 or Alt+Down opens the list. In an open list, press Enter, F4 or Alt+Down to take the highlighted choice.
    +A drop-down list shows one choice with an arrow button beside it. Click it to see all the choices, then click the one you want. With the keyboard, Up and Down change the choice, and Enter, the Space bar, F4, or Alt+Down opens the list. In an open list, press Enter, F4, or Alt+Down to take the highlighted choice.
     
     Some drop-down lists also let you type your own entry in the box.
     
    @@ -419,7 +419,7 @@ Click in a text box and type. These keys help you edit:
     
     You can also drag across text with the mouse to select it. Double-click selects a word, and a triple-click selects the whole line.
     
    -Cut, Copy and Paste work in every text box; see Copying and Pasting.
    +Cut, Copy, and Paste work in every text box; see Copying and Pasting.
     
     .link user.clipboard Copying and Pasting
     
    @@ -473,7 +473,7 @@ Double-click a folder to look inside it. Click a file to put its name in the Fil
     
     .h2 Saving a File
     
    -Go to the folder you want, type a name in the File name box and click Save. If you leave off the ending (such as .txt), the dialog adds the one from the Filter box. If a file with that name already exists, DVX asks whether to replace it.
    +Go to the folder you want, type a name in the File name box, and click Save. If you leave off the ending (such as .txt), the dialog adds the one from the Filter box. If a file with that name already exists, DVX asks whether to replace it.
     
     .note tip
     Alt+D opens the Drive list from the keyboard. You can also change drives by typing the drive letter and a colon, such as A:, in the Path box and pressing Enter.
    @@ -525,13 +525,13 @@ Programs that can print have Print... in their File menu, and programs that prin
     
     .h2 The Print Dialog
     
    -File > Print... opens the Print dialog. Choose where the printout goes (a printer port such as LPT1, or a file), how many copies you want and, for a document of several pages, whether to print all of it or only some pages. Then choose Print.
    +File > Print... opens the Print dialog. Choose where the printout goes (a printer port such as LPT1, or a file), how many copies you want, and (for a document of several pages) whether to print all of it or only some pages. Then choose Print.
     
     While a printout is going to the printer, a small Printing window shows it is busy. You can keep working in other programs, and Cancel stops the printout.
     
     .h2 When the Printer Has a Problem
     
    -If the printer is out of paper, switched off, off line or not answering, a message says what is wrong. Fix the problem and choose Retry to carry on, or Cancel to stop printing. Nothing waits forever.
    +If the printer is out of paper, switched off, off line, or not answering, a message says what is wrong. Fix the problem and choose Retry to carry on, or Cancel to stop printing. Nothing waits forever.
     
     .h2 Page Setup
     
    @@ -579,7 +579,7 @@ Most programs can only run once at a time. If you start a program that is alread
     .list
     .item Click anywhere in the program's window, or
     .item Press Alt+Tab until its window comes to the front, or
    -.item Press Ctrl+Esc to open the Task Manager, pick the program and click Switch To.
    +.item Press Ctrl+Esc to open the Task Manager, pick the program, and click Switch To.
     .endlist
     
     .h2 Programs That Use the Whole Screen
    @@ -626,7 +626,7 @@ The Program Manager opens when DVX starts and stays open the whole time DVX runs
     .item The status bar describes the selected program and says how many programs are running.
     .endlist
     
    -You can make your own groups, add, move and copy icons, give an icon a shortcut key that starts it from any program, and choose large icons, small icons or a list. Everything you arrange is remembered the next time you start DVX. The Program Manager's own help explains all of this: press F1 while it is the active window, or choose Contents from its Help menu.
    +You can make your own groups, add, move, and copy icons, give an icon a shortcut key that starts it from any program, and choose large icons, small icons, or a list. Everything you arrange is remembered the next time you start DVX. The Program Manager's own help explains all of this: press F1 while it is the active window, or choose Contents from its Help menu.
     
     .note warning
     Closing the Program Manager window leaves DVX, after asking you first.
    @@ -649,11 +649,11 @@ Closing the Program Manager window leaves DVX, after asking you first.
     
     .h1 Changing Settings
     
    -The mouse, the screen colors, the desktop picture, the screen resolution, which program opens each kind of document, the network, the printer, joysticks and the sounds are changed with the settings program, which has an icon in the Installed Programs group of the Program Manager. Start it like any other program. Its own help, opened with F1 or its Help menu, explains every setting.
    +The mouse, the screen colors, the desktop picture, the screen resolution, which program opens each kind of document, the network, the printer, joysticks, and the sounds are changed with the settings program, which has an icon in the Installed Programs group of the Program Manager. Start it like any other program. Its own help, opened with F1 or its Help menu, explains every setting.
     
     Settings you keep are saved, and DVX uses them again the next time it starts.
     
    -DVX plays a short sound when it starts and when it ends, and when a message appears (a different one for errors, warnings, questions and information). Each sound can be changed or turned off, and the volume set, in the settings program.
    +DVX plays a short sound when it starts and when it ends, and when a message appears (a different one for errors, warnings, questions, and information). Each sound can be changed or turned off, and the volume set, in the settings program.
     
     .note tip
     If a new screen resolution leaves the screen unreadable, just wait: DVX goes back to the old one after 15 seconds unless you confirm the change.
    @@ -825,17 +825,19 @@ You can pick another resolution at any time with the settings program (see Chang
     
     .h2 There is no sound
     
    -DVX finds the sound hardware by itself. A Sound Blaster needs its BLASTER setting in AUTOEXEC.BAT, for example SET BLASTER=A220 I5 D1 H5 (the card's manual gives the numbers). Without a sound card DVX uses the PC speaker, which plays tones and beeps but not recorded sounds. The settings program shows the sound hardware in use, lets you pick it yourself and sets the volume; if a sound is set to none there, nothing is heard for it. Without any sound hardware DVX and its programs work the same, silently.
    +DVX finds the sound hardware by itself. A Sound Blaster needs its BLASTER setting in AUTOEXEC.BAT, for example SET BLASTER=A220 I5 D1 H5 (the card's manual gives the numbers). Without a sound card DVX uses the PC speaker, which plays tones and beeps but not recorded sounds. The settings program shows the sound hardware in use, lets you pick it yourself, and sets the volume; if a sound is set to none there, nothing is heard for it. Without any sound hardware DVX and its programs work the same, silently.
     
     .h2 An empty floppy or CD-ROM drive
     
     When a program tries to read a drive that has no disk in it, or to write to a write-protected disk, the program simply reports that it could not read or write the file. DVX never stops at the DOS "Abort, Retry, Fail" question. Put the disk in and try again.
     
    -.h2 Installing, upgrading and removing DVX
    +.h2 Installing, upgrading, and removing DVX
     
    -DVX is installed with SETUP, from the DVX CD or from Disk 1 of the DVX floppy disks: change to that drive and type SETUP. Setup shows the licence, asks for the folder to install DVX in, checks that there is room and unpacks DVX there, asking for each floppy disk in turn. It can also make DVX start when the computer starts (it shows the line it adds to AUTOEXEC.BAT and keeps a copy of the old file as AUTOEXEC.DVX). Setup never writes to the CD or the floppy disks.
    +DVX is installed with SETUP, from the DVX CD or from Disk 1 of the DVX floppy disks: change to that drive and type SETUP. Setup shows the licence, asks for the folder to install DVX in and which fonts to install, checks that there is room, and unpacks DVX there, asking for each floppy disk it needs in turn. It can also make DVX start when the computer starts (it shows the line it adds to AUTOEXEC.BAT and keeps a copy of the old file as AUTOEXEC.DVX). Setup never writes to the CD or the floppy disks.
     
    -To upgrade or repair DVX, run Setup again and choose the same folder: answer Yes to keep your settings. To remove DVX, run SETUP in the DVX folder (for example C:\DVX\SETUP) and choose Uninstall. Files you added to the folder yourself are not removed, and your settings are only removed if you ask.
    +The fonts are up to you. None keeps DVX to its own font; the base set adds DejaVu Sans, Sans Mono, and Serif in all their styles (about 2 MB); and All adds every font that comes with DVX (about 8 MB). A program that asks for a font you did not install shows its text in DVX's own font instead, unless it carries that font itself (some programs do, such as Welcome and the DVX Demo). Every font file holds its own licence: the Licence button of the Font dialog shows it. With None or the base set, Setup does not ask for the last floppy disks, which hold only fonts.
    +
    +To upgrade or repair DVX, run Setup again and choose the same folder: answer Yes to keep your settings. Setup starts with the fonts that folder already has; choose others to add fonts or remove them. To remove DVX, run SETUP in the DVX folder (for example C:\DVX\SETUP) and choose Uninstall. Files you added to the folder yourself are not removed, and your settings are only removed if you ask.
     
     .h2 Running DVX from a CD-ROM or a write-protected disk
     
    @@ -881,7 +883,7 @@ DVX writes a record of what happened to the file DVX.LOG, in the same folder as
     
     .h1 For Developers
     
    -The rest of this help file is a technical reference for programmers who write programs, controls or add-ons for DVX in C. It is listed under Technical Reference (for Developers) in the table of contents.
    +The rest of this help file is a technical reference for programmers who write programs, controls, or add-ons for DVX in C. It is listed under Technical Reference (for Developers) in the table of contents.
     
     .link sys.welcome Start with the Developer Overview
     
    diff --git a/src/apps/kpunch/resedit/resedit.dhs b/src/apps/kpunch/resedit/resedit.dhs
    index d9b50d6e..33ea97e7 100644
    --- a/src/apps/kpunch/resedit/resedit.dhs
    +++ b/src/apps/kpunch/resedit/resedit.dhs
    @@ -32,9 +32,9 @@
     
     .h1 Resource Editor
     
    -Program files, control add-ons and libraries can carry extra pieces of information inside them, called resources: for example a program's icon, its name, its author and a short description. The Program Manager shows a program's icon and description from these resources.
    +Program files, control add-ons, and libraries can carry extra pieces of information inside them, called resources: for example a program's icon, its name, its author, and a short description. The Program Manager shows a program's icon and description from these resources.
     
    -The Resource Editor opens one of these files, lists its resources, and lets you add, change, save out or remove them. It is mainly a tool for people who make programs.
    +The Resource Editor opens one of these files, lists its resources, and lets you add, change, save out, or remove them. It is mainly a tool for people who make programs.
     
     .note warning
     Changes are written into the file straight away; there is no Save command and no Undo. Changing or removing the resources a program needs can stop it from working, so work on a copy of the file if you are not sure.
    @@ -54,7 +54,7 @@ Changes are written into the file straight away; there is no Save command and no
     
     .h2 Opening a File
     
    -Choose File > Open... and pick a program (.app), control add-on (.wgt) or library (.lib) file; use the Filter box to switch between them, or choose All Files. The list then shows each resource with its name, its type and its size. The line at the bottom shows the file name and how many resources it has. File > Close closes the file and empties the list.
    +Choose File > Open... and pick a program (.app), control add-on (.wgt), or library (.lib) file; use the Filter box to switch between them, or choose All Files. The list then shows each resource with its name, its type, and its size. The line at the bottom shows the file name and how many resources it has. File > Close closes the file and empties the list.
     
     A file named when the Resource Editor starts, for example in the Arguments of a Program Manager item for it, opens right away.
     
    @@ -99,4 +99,4 @@ Double-clicking a resource edits it if it is text, or saves it to a file if it i
       helpfile      In a DVX BASIC program, the name of the program's help file, which F1 opens.
     .endtable
     
    -Programs written in DVX BASIC also carry resources named MODULE and FORM0, FORM1 and so on. They hold the program itself; do not change or remove them.
    +Programs written in DVX BASIC also carry resources named MODULE and FORM0, FORM1, and so on. They hold the program itself; do not change or remove them.
    diff --git a/src/apps/kpunch/terminal/termSess.frm b/src/apps/kpunch/terminal/termSess.frm
    index 471bdfbf..228cbac0 100644
    --- a/src/apps/kpunch/terminal/termSess.frm
    +++ b/src/apps/kpunch/terminal/termSess.frm
    @@ -242,6 +242,9 @@ Begin Form SessionForm
                 Begin CheckBox chkEcho
                     Caption = "Local echo (show what you type)"
                 End
    +            Begin CheckBox chkMusic
    +                Caption = "Play ANSI music the other side sends"
    +            End
             End
         End
         Begin HBox boxButtons
    @@ -350,6 +353,7 @@ FUNCTION SessionDialog(BYVAL title AS STRING) AS BOOLEAN
         FillChoices ddScheme, SCHEME_LABELS, dlgSession.scheme
         txtAnswer.Text = dlgSession.answerback
         chkEcho.Value  = dlgSession.localEcho
    +    chkMusic.Value = dlgSession.ansiMusic
     
         txtName.SetFocus
         SessionForm.Show vbModal
    @@ -383,6 +387,7 @@ SUB cmdSessionOk_Click
         dlgSession.askPassphrase = chkAsk.Value
         dlgSession.secureNet     = chkSecureNet.Value
         dlgSession.localEcho     = chkEcho.Value
    +    dlgSession.ansiMusic     = chkMusic.Value
     
         dlgSession.initString = txtInit.Text
         dlgSession.dialPrefix = txtPrefix.Text
    diff --git a/src/apps/kpunch/terminal/terminal.bas b/src/apps/kpunch/terminal/terminal.bas
    index 50a7af7f..5dc3b973 100644
    --- a/src/apps/kpunch/terminal/terminal.bas
    +++ b/src/apps/kpunch/terminal/terminal.bas
    @@ -173,6 +173,7 @@ CONST KEY_ROWS = "rows"
     CONST KEY_SCROLLBACK = "scrollback"
     CONST KEY_SCHEME = "scheme"
     CONST KEY_ECHO = "localecho"
    +CONST KEY_MUSIC = "ansimusic"
     CONST KEY_ANSWERBACK = "answerback"
     
     ' A session: a connection plus how the screen looks while connected
    @@ -200,6 +201,7 @@ TYPE TermSession
         askPassphrase AS BOOLEAN   ' secure link: ask on connect, never store it
         secureNet AS BOOLEAN       ' secure link: over the network (host, tcpPort)
         localEcho AS BOOLEAN
    +    ansiMusic AS BOOLEAN       ' play the ANSI music the other side sends
         answerback AS STRING       ' reply to ENQ ("" = none)
     END TYPE
     
    @@ -223,6 +225,8 @@ DIM SHARED conn AS LONG
     DIM SHARED statusSerial AS LONG   ' the connection status shown last
     DIM SHARED lastState AS LONG      ' the connection state the menus follow
     DIM SHARED capturing AS BOOLEAN   ' received bytes go to a capture file
    +DIM SHARED musicPlaying AS BOOLEAN ' a tune from the other side is playing
    +DIM SHARED lastBell AS DOUBLE     ' TIMER when the bell last rang
     
     ' The file transfer on the connection (xfer.bas handle, 0 = none) and
     ' whether it still runs; whether its window is open
    @@ -428,6 +432,7 @@ SUB SessionDefaults(s AS TermSession)
         s.askPassphrase = False
         s.secureNet     = False
         s.localEcho     = False
    +    s.ansiMusic     = True
         s.answerback    = ""
     END SUB
     
    @@ -669,6 +674,7 @@ SUB LoadEntry(BYVAL ini AS LONG, BYVAL sec AS STRING, s AS TermSession)
         s.scrollback    = PrefsGetInt(ini, sec, KEY_SCROLLBACK, d.scrollback)
         s.scheme        = GetChoice(ini, sec, KEY_SCHEME, SCHEME_NAMES, d.scheme)
         s.localEcho     = PrefsGetBool(ini, sec, KEY_ECHO, d.localEcho)
    +    s.ansiMusic     = PrefsGetBool(ini, sec, KEY_MUSIC, d.ansiMusic)
         s.askPassphrase = PrefsGetBool(ini, sec, KEY_ASK_PASS, d.askPassphrase)
         s.secureNet     = PrefsGetBool(ini, sec, KEY_SECURE_NET, d.secureNet)
         s.initString    = PrefsGet$(ini, sec, KEY_INIT, d.initString)
    @@ -712,5 +718,6 @@ SUB SaveEntry(BYVAL ini AS LONG, BYVAL sec AS STRING, s AS TermSession)
         PrefsSetInt ini, sec, KEY_SCROLLBACK, s.scrollback
         PrefsSet ini, sec, KEY_SCHEME, ChoiceName$(SCHEME_NAMES, s.scheme)
         PrefsSetBool ini, sec, KEY_ECHO, s.localEcho
    +    PrefsSetBool ini, sec, KEY_MUSIC, s.ansiMusic
         PrefsSet ini, sec, KEY_ANSWERBACK, s.answerback
     END SUB
    diff --git a/src/apps/kpunch/terminal/terminal.dbp b/src/apps/kpunch/terminal/terminal.dbp
    index 48631bce..1cdefe5d 100644
    --- a/src/apps/kpunch/terminal/terminal.dbp
    +++ b/src/apps/kpunch/terminal/terminal.dbp
    @@ -4,7 +4,7 @@ Author = Scott Duensing
     Publisher = Kangaroo Punch Studios
     Version = 1.00
     Copyright = Copyright 2026 Scott Duensing
    -Description = Serial, modem and secure-link terminal with XMODEM, YMODEM and ZMODEM file transfer
    +Description = Serial, modem, and secure-link terminal with XMODEM, YMODEM, and ZMODEM file transfer
     Icon = ICON32.BMP
     
     [Modules]
    diff --git a/src/apps/kpunch/terminal/terminal.dhs b/src/apps/kpunch/terminal/terminal.dhs
    index 9f90512c..a554df52 100644
    --- a/src/apps/kpunch/terminal/terminal.dhs
    +++ b/src/apps/kpunch/terminal/terminal.dhs
    @@ -39,7 +39,7 @@ Terminal connects your computer to bulletin boards (BBSes) and other computers t
     
     .list
     .item Choose File > Phone Book... and click New....
    -.item Give the session a name, choose how to connect (Serial port, Modem, Secure link or Telnet) and the port, and set the speed and other settings to match the other side. For a modem, type the phone number on the Modem tab. For Telnet, type the host name and port on the Network tab.
    +.item Give the session a name, choose how to connect (Serial port, Modem, Secure link, or Telnet) and the port, and set the speed and other settings to match the other side. For a modem, type the phone number on the Modem tab. For Telnet, type the host name and port on the Network tab.
     .item Click OK, then Connect.
     .endlist
     
    @@ -86,9 +86,9 @@ Settings > Session Settings... changes the current session. Changes to the conne
       Port          COM1 to COM4.
       Speed (bps)   300 to 115200 bits per second. Both sides must use the same speed.
       Data bits     5 to 8; almost always 8.
    -  Parity        None, Even, Odd, Mark or Space; almost always None.
    +  Parity        None, Even, Odd, Mark, or Space; almost always None.
       Stop bits     1 or 2; almost always 1.
    -  Flow control  None, XON/XOFF or RTS/CTS. RTS/CTS is best when the cable and the other side support it, especially at high speeds and for file transfers.
    +  Flow control  None, XON/XOFF, or RTS/CTS. RTS/CTS is best when the cable and the other side support it, especially at high speeds and for file transfers.
     .endtable
     
     .h2 Modem Tab
    @@ -133,9 +133,10 @@ Telnet uses these settings, and so does the secure link when "Connect over the n
       -------           -------
       Columns, Rows     The size of the terminal screen (40 to 132 columns, 10 to 60 rows). 80 by 25 suits most bulletin boards.
       Scrollback lines  How many lines that scrolled off the top you can scroll back to.
    -  Colors            Standard, Green screen, Amber screen or Black on white. Also on the Settings > Colors menu.
    +  Colors            Standard, Green screen, Amber screen, or Black on white. Also on the Settings > Colors menu.
       Answerback        Text sent back when the other computer asks who you are (the ENQ character, Ctrl+E). Leave it empty to send nothing.
       Local echo        Also shows what you type. Turn it on if nothing appears when you type (the other side does not echo); turn it off if every letter appears twice.
    +  ANSI music        Plays the music some bulletin boards send. Also on the Settings > ANSI Music menu.
     .endtable
     
     The phone book and all settings are saved in the file TERMINAL.INI in Terminal's folder under CONFIG.
    @@ -165,7 +166,7 @@ The port opens at once. Whatever you type goes to the other computer, and whatev
     
     Terminal sends the setup command, waits for the modem to answer OK, then dials. The status bar shows each try, for example "Dialing 555-0100 (try 1 of 4)". When the line is busy or nobody answers, Terminal waits the redial time and dials again, up to the number of redial tries. "No dial tone" stops at once: check that the phone line is plugged in.
     
    -When the call connects, the status bar shows the modem's CONNECT message. If the call drops (the modem loses the carrier), Terminal says "Carrier lost: the call ended". Disconnect hangs up. Everything the modem says (OK, CONNECT, BUSY and so on) also appears on the screen.
    +When the call connects, the status bar shows the modem's CONNECT message. If the call drops (the modem loses the carrier), Terminal says "Carrier lost: the call ended". Disconnect hangs up. Everything the modem says (OK, CONNECT, BUSY, and so on) also appears on the screen.
     
     With no phone number, Terminal opens the port and you type modem commands yourself, for example ATDT5550100 followed by Enter.
     
    @@ -195,10 +196,13 @@ Terminal can also be started with a Telnet address, telnet://host or telnet://ho
     .index Colors
     .index Send Break
     .index Clear Screen
    +.index ANSI Music
    +.index Music
    +.index Bell
     
     .h1 Using the Terminal Screen
     
    -Click the screen and type: every key goes to the other computer, including Ctrl combinations, Tab, Esc and the arrow keys. Terminal understands the ANSI colors and cursor movement bulletin boards use.
    +Click the screen and type: every key goes to the other computer, including Ctrl combinations, Tab, Esc, and the arrow keys. Terminal understands the ANSI colors and cursor movement bulletin boards use.
     
     .h2 Scrolling Back
     
    @@ -210,6 +214,12 @@ Drag across the screen to select text; double-click selects a word and triple-cl
     
     Edit > Paste (or Ctrl+V) sends the clipboard to the other computer as if you had typed it.
     
    +.h2 Music and the Bell
    +
    +Some bulletin boards play tunes (ANSI music) on their menus and welcome screens. Terminal plays them through the sound card, or through the PC speaker when there is none. While a tune plays, the screen waits for it to end before showing more, the way the bulletin board meant it.
    +
    +Settings > ANSI Music turns the music off or on for the current session, and turning it off stops a tune that is playing. When the other computer rings the bell, Terminal beeps.
    +
     .h2 Other Commands
     
     .table
    @@ -218,6 +228,7 @@ Edit > Paste (or Ctrl+V) sends the clipboard to the other computer as if you had
       Edit > Clear Screen    Clears the screen (the old screen goes to the scrollback).
       Edit > Send Break      Sends a break signal, which some systems use to interrupt. Serial port and modem only.
       Settings > Local Echo  Also shows what you type.
    +  Settings > ANSI Music  Plays or silences the music bulletin boards send.
       Settings > Colors      Standard colors, a green or amber screen, or black text on white.
     .endtable
     
    @@ -315,7 +326,7 @@ On a serial port or modem without flow control, ZMODEM downloads automatically a
     .table
       Menu      Command              What it does
       ----      -------              ------------
    -  File      Phone Book...        Choose, add, change or delete sessions.
    +  File      Phone Book...        Choose, add, change, or delete sessions.
       File      Connect              Connect with the current session.
       File      Disconnect           End the connection (a modem hangs up).
       File      Capture to File...   Start or stop saving what you receive.
    @@ -326,6 +337,7 @@ On a serial port or modem without flow control, ZMODEM downloads automatically a
       Edit      Send Break           Send a break signal.
       Settings  Session Settings...  Change the current session.
       Settings  Local Echo           Show what you type.
    +  Settings  ANSI Music           Play the music the other computer sends.
       Settings  Colors               Choose the screen colors.
       Settings  Transfer Options...  Download folder and transfer behavior.
       Transfer  Send Files...        Send files with the checked protocol.
    @@ -347,7 +359,7 @@ On a serial port or modem without flow control, ZMODEM downloads automatically a
       ------------                                             -----------
       COM1 could not be opened: there is no serial port there  Choose another port in Session Settings; the computer has no serial port at that address.
       COM1 could not be opened: it is already in use           Another program (or another Terminal window) is using that port. Close it first.
    -  The screen shows random characters                       The speed, data bits, parity or stop bits do not match the other side.
    +  The screen shows random characters                       The speed, data bits, parity, or stop bits do not match the other side.
       Nothing appears when you type                            Turn on Settings > Local Echo.
       Every letter appears twice                               Turn off Settings > Local Echo.
       The modem did not answer                                 Check that the modem is on and connected to the chosen port, and that the setup command is right.
    diff --git a/src/apps/kpunch/terminal/terminal.frm b/src/apps/kpunch/terminal/terminal.frm
    index 6a737166..7d87abaa 100644
    --- a/src/apps/kpunch/terminal/terminal.frm
    +++ b/src/apps/kpunch/terminal/terminal.frm
    @@ -76,6 +76,9 @@ Begin Form TermForm
             Begin Menu mnuEcho
                 Caption = "&Local Echo"
             End
    +        Begin Menu mnuMusic
    +            Caption = "ANSI &Music"
    +        End
             Begin Menu mnuSettingsSep1
                 Caption = "-"
             End
    @@ -178,6 +181,11 @@ End
     ' so the screen, the menus and the other programs stay usable while a
     ' modem dials, a secure link is set up or a file is transferred.
     '
    +' ANSI music the other side sends comes as the terminal's Music event and
    +' plays through the music library (the sound card, else the PC speaker).
    +' A tune in the foreground (MF) holds the screen until it ends: the
    +' terminal takes nothing in and the timer leaves the connection alone.
    +'
     ' The phone book (terminal.bas) is the list of saved sessions; the window
     ' shows one of them (session) and its title is the session's name.
     ' Started with "telnet://host[:port]", Terminal uses (or adds) that
    @@ -195,7 +203,7 @@ CONST NOTE_START = "["
     CONST NOTE_END = "]"
     CONST CRLF = CHR$(13) & CHR$(10)
     
    -CONST ABOUT_TEXT = "Terminal" & CHR$(10) & CHR$(10) & "Connects to bulletin boards and other computers over a serial port, a modem or the secure link, and transfers files with XMODEM, YMODEM and ZMODEM." & CHR$(10) & CHR$(10) & "Copyright 2026 Scott Duensing"
    +CONST ABOUT_TEXT = "Terminal" & CHR$(10) & CHR$(10) & "Connects to bulletin boards and other computers over a serial port, a modem, or the secure link, and transfers files with XMODEM, YMODEM, and ZMODEM." & CHR$(10) & CHR$(10) & "Copyright 2026 Scott Duensing"
     
     ' Color schemes: the 16 text attribute colors.  The one-color schemes
     ' scale a tint by each attribute's brightness; black on white swaps the
    @@ -208,6 +216,11 @@ CONST AMBER_TINT = "255|176|0"
     CONST LIGHT_COLORS = "255,255,255|0,0,170|0,128,0|0,128,128|170,0,0|170,0,170|128,64,0|0,0,0|96,96,96|0,0,255|0,160,0|0,140,140|210,0,0|190,0,190|150,110,0|0,0,0"
     CONST RGB_SEP = ","
     
    +' The bell rings at most once in this many seconds
    +CONST BELL_GAP = 0.5
    +' SOUND's lowest frequency: with no length, it silences what is playing
    +CONST SILENCE_HZ = 37
    +
     
     ' ============================================================
     ' Window
    @@ -298,7 +311,11 @@ END SUB
     
     ' The heartbeat: connection, transfer, screen, status.
     SUB tmrTick_Timer
    -    ConnPoll conn
    +    ' A tune playing holds the terminal: what arrives meanwhile waits in
    +    ' the connection
    +    IF NOT musicPlaying THEN
    +        ConnPoll conn
    +    END IF
     
         IF transferring THEN
             ProgressUpdate
    @@ -310,12 +327,43 @@ SUB tmrTick_Timer
     END SUB
     
     
    +' ANSI music from the other side, when the session plays it.  PLAY in the
    +' foreground (MF, the usual) returns when the tune ends; until then the
    +' terminal takes nothing in and the timer leaves the connection alone.  A
    +' mistake in the tune is ignored.
    +SUB trmMain_Music(Macro AS STRING)
    +    IF NOT session.ansiMusic THEN
    +        EXIT SUB
    +    END IF
    +
    +    ON ERROR RESUME NEXT
    +    musicPlaying = True
    +    PLAY Macro
    +    musicPlaying = False
    +END SUB
    +
    +
    +' A bell from the other side: a beep, at most one every BELL_GAP seconds
    +' (TIMER starts again at midnight)
    +SUB trmMain_Bell
    +    DIM stamp AS DOUBLE
    +
    +    stamp = TIMER
    +
    +    IF stamp < lastBell OR stamp - lastBell >= BELL_GAP THEN
    +        lastBell = stamp
    +        BEEP
    +    END IF
    +END SUB
    +
    +
     ' ============================================================
     ' The session
     ' ============================================================
     
    -' Scrollback, colors and local echo of the session, on the terminal, the
    -' menus and the connection.
    +' Scrollback, colors, local echo, and ANSI music of the session, on the
    +' terminal, the menus, and the connection (music off silences a tune
    +' that is playing).
     SUB ApplyLook
         trmMain.Scrollback = session.scrollback
         SchemeColors session.scheme
    @@ -325,9 +373,14 @@ SUB ApplyLook
         mnuSchemeAmber.Checked    = (session.scheme = SCHEME_AMBER)
         mnuSchemeLight.Checked    = (session.scheme = SCHEME_LIGHT)
         mnuEcho.Checked           = session.localEcho
    +    mnuMusic.Checked          = session.ansiMusic
     
         ConnSetEcho conn, session.localEcho
         ConnSetAnswerback conn, session.answerback
    +
    +    IF NOT session.ansiMusic THEN
    +        SOUND SILENCE_HZ, 0
    +    END IF
     END SUB
     
     
    @@ -827,6 +880,14 @@ SUB mnuEcho_Click
     END SUB
     
     
    +SUB mnuMusic_Click
    +    session.ansiMusic = NOT session.ansiMusic
    +    ApplyLook
    +    SessionStore
    +    MenuUpdate
    +END SUB
    +
    +
     SUB mnuXferOptions_Click
         IF XferOptionsDialog() THEN
             SaveBook
    diff --git a/src/apps/kpunch/welcome/fonts.ini b/src/apps/kpunch/welcome/fonts.ini
    new file mode 100644
    index 00000000..f61e1c8d
    --- /dev/null
    +++ b/src/apps/kpunch/welcome/fonts.ini
    @@ -0,0 +1,30 @@
    +; fonts.ini -- the fonts Welcome carries (made by mkfont at build time;
    +; welcome.dbp lists them in [Fonts], so they are font resources of
    +; welcome.app): DejaVu Sans Bold for the banner's title (36 pixels) and
    +; the tour's titles (24), DejaVu Sans for the lines under them (14).
    +; Smoothed, with only the characters Welcome shows, so it looks the same
    +; whichever fonts were installed.
    +
    +[fonts]
    +WELCOMEB = DejaVu Sans
    +WELCOME  = DejaVu Sans
    +
    +[licences]
    +DEJAVU = dejavu/dejavu-fonts-ttf-2.37/LICENSE
    +
    +[WELCOMEB]
    +style    = bold
    +licence  = DEJAVU
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans-Bold.ttf
    +sizes    = 24 36
    +smooth   = all
    +hinting  = native
    +chars    = 20-7E
    +
    +[WELCOME]
    +licence  = DEJAVU
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans.ttf
    +sizes    = 14
    +smooth   = all
    +hinting  = native
    +chars    = 20-7E
    diff --git a/src/apps/kpunch/welcome/welcome.dbp b/src/apps/kpunch/welcome/welcome.dbp
    index e966c03c..6a08b6e9 100644
    --- a/src/apps/kpunch/welcome/welcome.dbp
    +++ b/src/apps/kpunch/welcome/welcome.dbp
    @@ -16,5 +16,9 @@ Count = 2
     File0 = welcome.frm
     Count = 1
     
    +[Fonts]
    +File0 = ../../../../obj/apps/welcome/fonts/WELCOMEB.DFN
    +File1 = ../../../../obj/apps/welcome/fonts/WELCOME.DFN
    +
     [Settings]
     StartupForm = WelcomeForm
    diff --git a/src/apps/kpunch/welcome/welcome.dhs b/src/apps/kpunch/welcome/welcome.dhs
    index 351f686c..6b250d83 100644
    --- a/src/apps/kpunch/welcome/welcome.dhs
    +++ b/src/apps/kpunch/welcome/welcome.dhs
    @@ -42,12 +42,12 @@ The Welcome window appears when DVX starts for the first time. It shows what DVX
     .table
       Tile                   What it does
       ----                   ------------
    -  Take the tour          Shows a short tour of DVX in the window: Program Manager, windows, files, Control Panel, DVX BASIC and help, a page at a time. Next and Previous move through it; End the tour brings the tiles back.
    +  Take the tour          Shows a short tour of DVX in the window: Program Manager, windows, files, Control Panel, DVX BASIC, and help, a page at a time. Next and Previous move through it; End the tour brings the tiles back.
       Write a program        Starts DVX BASIC, where you draw forms and write your own programs.
    -  Make it yours          Starts Control Panel: colors, wallpaper, the mouse, sounds and more.
    +  Make it yours          Starts Control Panel: colors, wallpaper, the mouse, sounds, and more.
       Browse your files      Starts File Manager.
       See it in action       Starts the DVX Demo, which shows the windows and controls DVX has.
    -  Read the User Guide    Opens the DVX help, which explains the desktop, windows, menus and the programs that come with DVX.
    +  Read the User Guide    Opens the DVX help, which explains the desktop, windows, menus, and the programs that come with DVX.
     .endtable
     
     Click a tile's picture or its button, or press Alt with the underlined letter. Under the tiles a tip about DVX changes every few seconds. Close closes the Welcome window.
    diff --git a/src/apps/kpunch/welcome/welcome.frm b/src/apps/kpunch/welcome/welcome.frm
    index 197ae84b..ab2012c8 100644
    --- a/src/apps/kpunch/welcome/welcome.frm
    +++ b/src/apps/kpunch/welcome/welcome.frm
    @@ -55,7 +55,7 @@ Begin Form WelcomeForm
                             Caption = "&Make it yours"
                         End
                         Begin Label lblLookTile
    -                        Caption = "Colors, wallpaper and more"
    +                        Caption = "Colors, wallpaper, and more"
                         End
                     End
                 End
    @@ -198,11 +198,13 @@ End
     '
     ' The shell starts the program DVX.INI [shell] welcome names once the
     ' desktop is up; the DVX.INI DVX ships with names this one.  A banner
    -' (a gradient and a big title, FontScale) greets the user; six tiles
    +' (a gradient and a big title) greets the user; six tiles
     ' start the tour, DVX BASIC, Control Panel, File Manager, the DVX Demo
     ' and the user guide; a tip below them changes every few seconds.  The
     ' tour shows its pages in place of the tiles: each a program's icon, a
    -' title and a few lines, with Previous and Next.
    +' title and a few lines, with Previous and Next.  The banner and the tour
    +' are drawn in fonts the program carries (fonts.ini, the project's
    +' [Fonts]), so they look the same whichever fonts were installed.
     '
     ' "Don't show this again" starts ticked: closing the window then removes
     ' the setting, so the window is seen once.  Unticking it writes the setting
    @@ -237,18 +239,25 @@ CONST PALE         = &HFFD8C8
     CONST SHADOW       = &H000000
     CONST PLATE_FACE   = &HC0C0C0
     
    -' The banner: the title's size and place, the tagline under it
    -CONST TITLE_SCALE  = 3
    +' The fonts Welcome carries: their family, the banner's title size, a
    +' tour page's title size, the text's size, and the titles' shadows
    +CONST FONT_FAMILY  = "DejaVu Sans"
    +CONST TITLE_SIZE   = 36
    +CONST TOUR_SIZE    = 24
    +CONST TEXT_SIZE    = 14
    +CONST TITLE_SHADOW = 3
    +CONST TOUR_SHADOW  = 2
    +
    +' The banner: the title's place, the tagline under it
     CONST BANNER_X     = 16
     CONST BANNER_Y     = 10
     CONST BANNER_GAP   = 6
     
    -' The tour: pages, lines a page, a title's size; the icon's plate (a
    -' raised bevel round the icon drawn ICON_SCALE times its size) and where
    -' the text starts
    +' The tour: pages, the most lines a page's text wraps to; the icon's
    +' plate (a raised bevel round the icon drawn ICON_SCALE times its size)
    +' and where the text starts
     CONST TOUR_PAGES   = 7
    -CONST TOUR_LINES   = 3
    -CONST TOUR_SCALE   = 2
    +CONST TOUR_LINES   = 6
     CONST TOUR_PAD     = 14
     CONST ICON_SIZE    = 32
     CONST ICON_SCALE   = 2
    @@ -263,7 +272,8 @@ DIM SHARED tourPage AS INTEGER
     DIM SHARED tipIndex AS INTEGER
     DIM SHARED tourTitle(1 TO TOUR_PAGES) AS STRING
     DIM SHARED tourIcon(1 TO TOUR_PAGES) AS STRING
    -DIM SHARED tourLine(1 TO TOUR_PAGES, 1 TO TOUR_LINES) AS STRING
    +DIM SHARED tourText(1 TO TOUR_PAGES) AS STRING
    +DIM SHARED wrapped(1 TO TOUR_LINES) AS STRING
     DIM SHARED tips(1 TO TIP_COUNT) AS STRING
     
     
    @@ -312,24 +322,23 @@ END FUNCTION
     
     ' The tour's pages: each a title, the program whose icon it shows (or
     ' several, separated by "|": Program Manager has no icon of its own, its
    -' page shows a few of the programs it starts) and TOUR_LINES lines.
    +' page shows a few of the programs it starts) and its text (wrapped to
    +' the page when shown).
     SUB FillTour
    -    SetPage 1, "A quick tour of DVX", ShellAppPath$(), "DVX turns your DOS computer into a desktop of", "windows, menus and icons. This tour shows the", "main parts. Press Next to begin."
    -    SetPage 2, "Program Manager", ProgramPath$(APP_CLOCK) + "|" + ProgramPath$(APP_NOTEPAD) + "|" + ProgramPath$(APP_IMAGES) + "|" + ProgramPath$(APP_ICONS), "Your programs live in groups. Double-click an", "icon to start it, or type its name in Search.", "File > New Group makes groups of your own."
    -    SetPage 3, "Windows", ProgramPath$(APP_DEMO), "Every program runs in a window. Drag the title", "bar to move it, or an edge to change its size.", "Alt+Tab switches windows; Alt+F4 closes one."
    -    SetPage 4, "Your files", ProgramPath$(APP_FILES), "File Manager shows your disks and folders. Copy,", "move, rename and delete files with the mouse or", "the keys; double-click a file to open it."
    -    SetPage 5, "Make it yours", ProgramPath$(APP_CPANEL), "Control Panel sets colors and themes, wallpaper,", "the mouse, sounds, joysticks, printers and the", "screen. Most changes show right away."
    -    SetPage 6, "Write your own programs", ProgramPath$(APP_BASIC), "DVX BASIC is all you need to build programs: draw", "a form, add buttons and lists, write some code", "and press F5. Make an .app to share it."
    -    SetPage 7, "Help is always there", ProgramPath$(APP_HELP), "Press F1 in any program for help about it. The", "Help Viewer holds every guide, with an index and", "a search. Have fun with DVX!"
    +    SetPage 1, "A quick tour of DVX", ShellAppPath$(), "DVX turns your DOS computer into a desktop of windows, menus, and icons. This tour shows the main parts. Press Next to begin."
    +    SetPage 2, "Program Manager", ProgramPath$(APP_CLOCK) + "|" + ProgramPath$(APP_NOTEPAD) + "|" + ProgramPath$(APP_IMAGES) + "|" + ProgramPath$(APP_ICONS), "Your programs live in groups. Double-click an icon to start it, or type its name in Search. File > New Group makes groups of your own."
    +    SetPage 3, "Windows", ProgramPath$(APP_DEMO), "Every program runs in a window. Drag the title bar to move it, or an edge to change its size. Alt+Tab switches windows; Alt+F4 closes one."
    +    SetPage 4, "Your files", ProgramPath$(APP_FILES), "File Manager shows your disks and folders. Copy, move, rename, and delete files with the mouse or the keys; double-click a file to open it."
    +    SetPage 5, "Make it yours", ProgramPath$(APP_CPANEL), "Control Panel sets colors and themes, wallpaper, the mouse, sounds, joysticks, printers, and the screen. Most changes show right away."
    +    SetPage 6, "Write your own programs", ProgramPath$(APP_BASIC), "DVX BASIC is all you need to build programs: draw a form, add buttons and lists, write some code, and press F5. Make an .app to share it."
    +    SetPage 7, "Help is always there", ProgramPath$(APP_HELP), "Press F1 in any program for help about it. The Help Viewer holds every guide, with an index and a search. Have fun with DVX!"
     END SUB
     
     
    -SUB SetPage(BYVAL page AS INTEGER, BYVAL title AS STRING, BYVAL icon AS STRING, BYVAL line1 AS STRING, BYVAL line2 AS STRING, BYVAL line3 AS STRING)
    -    tourTitle(page)   = title
    -    tourIcon(page)    = icon
    -    tourLine(page, 1) = line1
    -    tourLine(page, 2) = line2
    -    tourLine(page, 3) = line3
    +SUB SetPage(BYVAL page AS INTEGER, BYVAL title AS STRING, BYVAL icon AS STRING, BYVAL text AS STRING)
    +    tourTitle(page) = title
    +    tourIcon(page)  = icon
    +    tourText(page)  = text
     END SUB
     
     
    @@ -339,7 +348,7 @@ SUB FillTips
         tips(3) = "Did you know? Type in Program Manager's Search box to find a program."
         tips(4) = "Did you know? Ctrl+Esc opens the Task Manager."
         tips(5) = "Did you know? Drag a window's edge or corner to change its size."
    -    tips(6) = "Did you know? Control Panel changes colors, wallpaper and the mouse."
    +    tips(6) = "Did you know? Control Panel changes colors, wallpaper, and the mouse."
     END SUB
     
     
    @@ -356,28 +365,82 @@ SUB DrawGradient(BYVAL pic AS Control)
     END SUB
     
     
    -' Text scale times the font's size in colour at x, y, over a black shadow
    -' scale dots down and to the right.
    -SUB ShadowPrint(BYVAL pic AS Control, BYVAL x AS LONG, BYVAL y AS LONG, BYVAL text AS STRING, BYVAL scale AS INTEGER, BYVAL colour AS LONG)
    -    pic.FontScale = scale
    +' Text in the bold font at size, in colour at x, y, over a black shadow
    +' drop dots down and to the right.
    +SUB ShadowPrint(BYVAL pic AS Control, BYVAL x AS LONG, BYVAL y AS LONG, BYVAL text AS STRING, BYVAL size AS INTEGER, BYVAL drop AS INTEGER, BYVAL colour AS LONG)
    +    UseFont pic, size, True
         pic.ForeColor = SHADOW
    -    pic.CurrentX  = x + scale
    -    pic.CurrentY  = y + scale
    +    pic.CurrentX  = x + drop
    +    pic.CurrentY  = y + drop
         pic.Print text;
         pic.ForeColor = colour
         pic.CurrentX  = x
         pic.CurrentY  = y
         pic.Print text;
    -    pic.FontScale = 1
    +END SUB
    +
    +
    +' text cut into lines no wider than maxWidth in pic's font, into wrapped():
    +' how many (at most TOUR_LINES; a word too wide for a line has one of its
    +' own).
    +FUNCTION WrapText(BYVAL pic AS Control, BYVAL text AS STRING, BYVAL maxWidth AS LONG) AS INTEGER
    +    DIM count AS INTEGER
    +    DIM gap AS INTEGER
    +    DIM word AS STRING
    +    DIM row AS STRING
    +
    +    DO WHILE text <> ""
    +        gap = INSTR(text, " ")
    +
    +        IF gap = 0 THEN
    +            word = text
    +            text = ""
    +        ELSE
    +            word = LEFT$(text, gap - 1)
    +            text = MID$(text, gap + 1)
    +        END IF
    +
    +        IF row = "" THEN
    +            row = word
    +        ELSEIF pic.TextWidth(row + " " + word) <= maxWidth THEN
    +            row = row + " " + word
    +        ELSE
    +            IF count < TOUR_LINES THEN
    +                count = count + 1
    +                wrapped(count) = row
    +            END IF
    +
    +            row = word
    +        END IF
    +    LOOP
    +
    +    IF row <> "" AND count < TOUR_LINES THEN
    +        count = count + 1
    +        wrapped(count) = row
    +    END IF
    +
    +    WrapText = count
    +END FUNCTION
    +
    +
    +' pic's text in Welcome's family at size, bold or not.
    +SUB UseFont(BYVAL pic AS Control, BYVAL size AS INTEGER, BYVAL bold AS BOOLEAN)
    +    pic.FontName = FONT_FAMILY
    +    pic.FontSize = size
    +    pic.FontBold = bold
     END SUB
     
     
     SUB DrawBanner
    +    DIM titleHeight AS INTEGER
    +
         DrawGradient picBanner
    -    ShadowPrint picBanner, BANNER_X, BANNER_Y, "Welcome to DVX!", TITLE_SCALE, GOLD
    +    ShadowPrint picBanner, BANNER_X, BANNER_Y, "Welcome to DVX!", TITLE_SIZE, TITLE_SHADOW, GOLD
    +    titleHeight = picBanner.TextHeight("A")
    +    UseFont picBanner, TEXT_SIZE, False
         picBanner.ForeColor = PALE
         picBanner.CurrentX  = BANNER_X
    -    picBanner.CurrentY  = BANNER_Y + picBanner.TextHeight("A") * TITLE_SCALE + BANNER_GAP
    +    picBanner.CurrentY  = BANNER_Y + titleHeight + BANNER_GAP
         picBanner.Print "Your desktop for DOS. Here is where to start."
     END SUB
     
    @@ -390,21 +453,28 @@ SUB ShowTour(BYVAL page AS INTEGER)
         DIM side AS INTEGER
         DIM iconTop AS INTEGER
         DIM textTop AS INTEGER
    +    DIM titleHeight AS INTEGER
    +    DIM lineCount AS INTEGER
     
         tourPage = page
         side     = ICON_SIZE * ICON_SCALE
         iconTop  = (picTour.ScaleHeight - side) \ 2
    -    textTop  = (picTour.ScaleHeight - picTour.TextHeight("A") * (TOUR_SCALE + TOUR_LINES) - TOUR_GAP) \ 2
    +    UseFont picTour, TOUR_SIZE, True
    +    titleHeight = picTour.TextHeight("A")
    +    UseFont picTour, TEXT_SIZE, False
    +    lineCount = WrapText(picTour, tourText(page), picTour.ScaleWidth - TOUR_TEXT_X - TOUR_PAD)
    +    textTop   = (picTour.ScaleHeight - titleHeight - picTour.TextHeight("A") * lineCount - TOUR_GAP) \ 2
         DrawGradient picTour
         picTour.DrawBevel TOUR_PAD - PLATE_BEVEL, iconTop - PLATE_BEVEL, side + PLATE_BEVEL * 2, side + PLATE_BEVEL * 2, False, PLATE_FACE
         DrawIcons tourIcon(page), TOUR_PAD, iconTop
    -    ShadowPrint picTour, TOUR_TEXT_X, textTop, tourTitle(page), TOUR_SCALE, WHITE
    +    ShadowPrint picTour, TOUR_TEXT_X, textTop, tourTitle(page), TOUR_SIZE, TOUR_SHADOW, WHITE
    +    UseFont picTour, TEXT_SIZE, False
         picTour.ForeColor = PALE
    -    picTour.CurrentY  = textTop + picTour.TextHeight("A") * TOUR_SCALE + TOUR_GAP
    +    picTour.CurrentY  = textTop + titleHeight + TOUR_GAP
     
    -    FOR i = 1 TO TOUR_LINES
    +    FOR i = 1 TO lineCount
             picTour.CurrentX = TOUR_TEXT_X
    -        picTour.Print tourLine(page, i)
    +        picTour.Print wrapped(i)
         NEXT
     
         lblPage.Caption = "Page " + LTRIM$(STR$(page)) + " of " + LTRIM$(STR$(TOUR_PAGES))
    diff --git a/src/common.mk b/src/common.mk
    index e263d80a..3493a3f9 100644
    --- a/src/common.mk
    +++ b/src/common.mk
    @@ -31,6 +31,12 @@
     #                 in toolchains/djgpp.
     #   CWSDSTUB      the CWSDPMI stub prepended to DOS executables; same rules,
     #                 defaulting to toolchains/cwsdpmi/bin/bin/CWSDSTUB.EXE.
    +#   FREETYPE_DIR  FreeType for the font converter (mkfont); same rules,
    +#                 defaulting to toolchains/freetype.
    +#   UPX           the executable packer SETUP.EXE is compressed with; same
    +#                 rules, defaulting to toolchains/upx/upx.
    +#   FONT_SOURCES  the fonts mkfont converts; same rules, defaulting to
    +#                 toolchains/fonts (src/fonts/installFonts.sh).
     #   CC, DXE3GEN, EXE2COFF are derived from DJGPP_PREFIX.  DXE3GEN runs
     #   dxe3gen through src/tools/dxeGen.sh, which gives every link a private
     #   scratch directory (dxe3gen's fixed temp file names in the current
    @@ -77,6 +83,15 @@ endif
     ifeq ($(origin CWSDSTUB),undefined)
     CWSDSTUB         := $(DVX_TOOLCHAINS)/cwsdpmi/bin/bin/CWSDSTUB.EXE
     endif
    +ifeq ($(origin FREETYPE_DIR),undefined)
    +FREETYPE_DIR     := $(DVX_TOOLCHAINS)/freetype
    +endif
    +ifeq ($(origin FONT_SOURCES),undefined)
    +FONT_SOURCES     := $(DVX_TOOLCHAINS)/fonts
    +endif
    +ifeq ($(origin UPX),undefined)
    +UPX              := $(DVX_TOOLCHAINS)/upx/upx
    +endif
     CC                = $(DJGPP_PREFIX)/bin/$(DJGPP_TRIPLE)-gcc
     DXE3GEN           = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/$(DJGPP_TRIPLE) $(DVX_ROOT)/src/tools/dxeGen.sh $(DJGPP_PREFIX)/$(DJGPP_TRIPLE)/bin/dxe3gen
     EXE2COFF          = $(DJGPP_PREFIX)/$(DJGPP_TRIPLE)/bin/exe2coff
    diff --git a/src/fonts/fonts.ini b/src/fonts/fonts.ini
    new file mode 100644
    index 00000000..327e4f10
    --- /dev/null
    +++ b/src/fonts/fonts.ini
    @@ -0,0 +1,1578 @@
    +; fonts.ini -- the fonts DVX ships, made by mkfont (src/tools/mkfont.c,
    +; whose header documents every key) into bin/fonts at build time.
    +;
    +; [fonts] lists the .DFN files in order: 8.3 base name = family name.
    +; [licences] names the licences and the texts under toolchains/fonts each
    +; is made of; every .DFN carries its licence's text and its sources'
    +; copyright notices, so they go wherever the font does.  Each font has a
    +; section: its style, licence, and sources (paths under toolchains/fonts,
    +; where src/fonts/installFonts.sh puts the pinned downloads), each with
    +; the pixel sizes to make ("native": a bitmap face's own size) and how
    +; it is hinted.  smooth lists the sizes also made smoothed (anti-aliased,
    +; drawn when DVX smooths text): the DejaVu Sans, Sans Mono, and Serif
    +; text faces from 12 pixels and the display faces from 24, where it reads
    +; best.  A smoothed size takes about twice the room of its 1-bit one.
    +; graycut lists the sizes whose 1-bit glyphs are cut from the smoothed
    +; rendering instead of the face's own 1-bit hinting, which draws DejaVu
    +; Serif Bold near regular weight at 15, 16, and 20 pixels and breaks
    +; strokes of DejaVu Sans Bold at 8: Serif Bold at its smoothed sizes (so
    +; text without smoothing keeps their weight) and Sans Bold at 8.  Sans
    +; Mono Bold keeps its hinting (cut from gray, its small 0 fills in).
    +;
    +; set = base marks the base set: DejaVu Sans, Sans Mono, and Serif in
    +; every style (DVX draws a character a font lacks from the first two).
    +; Setup installs no fonts (DVX's own only), the base set, or all of them;
    +; the base set's files come first in the package (mkfont -l lists them).
    +;
    +; Names: an 8.3 base name is the family's short form plus B (bold), I
    +; (italic), or Z (bold italic).  Swiss, Roman, Schoolbook, Typewriter,
    +; and Ledger are the X11 Helvetica, Times, New Century Schoolbook, and
    +; Courier bitmaps and Bitstream Charter under neutral names: their own
    +; names are trademarks.
    +
    +[fonts]
    +; Outline text faces
    +DJVSANS  = DejaVu Sans
    +DJVSANSB = DejaVu Sans
    +DJVSANSI = DejaVu Sans
    +DJVSANSZ = DejaVu Sans
    +DJVSCND  = DejaVu Sans Condensed
    +DJVSCNDB = DejaVu Sans Condensed
    +DJVSCNDI = DejaVu Sans Condensed
    +DJVSCNDZ = DejaVu Sans Condensed
    +DJVMONO  = DejaVu Sans Mono
    +DJVMONOB = DejaVu Sans Mono
    +DJVMONOI = DejaVu Sans Mono
    +DJVMONOZ = DejaVu Sans Mono
    +DJVSERF  = DejaVu Serif
    +DJVSERFB = DejaVu Serif
    +DJVSERFI = DejaVu Serif
    +DJVSERFZ = DejaVu Serif
    +DJVRCND  = DejaVu Serif Condensed
    +DJVRCNDB = DejaVu Serif Condensed
    +DJVRCNDI = DejaVu Serif Condensed
    +DJVRCNDZ = DejaVu Serif Condensed
    +ARIMO    = Arimo
    +ARIMOB   = Arimo
    +ARIMOI   = Arimo
    +ARIMOZ   = Arimo
    +TINOS    = Tinos
    +TINOSB   = Tinos
    +TINOSI   = Tinos
    +TINOSZ   = Tinos
    +COUSINE  = Cousine
    +COUSINEB = Cousine
    +COUSINEI = Cousine
    +COUSINEZ = Cousine
    +CLRSANS  = Clear Sans
    +CLRSANSB = Clear Sans
    +CLRSANSI = Clear Sans
    +CLRSANSZ = Clear Sans
    +GO       = Go
    +GOB      = Go
    +GOI      = Go
    +GOZ      = Go
    +GOMONO   = Go Mono
    +GOMONOB  = Go Mono
    +GOMONOI  = Go Mono
    +GOMONOZ  = Go Mono
    +DRDSERF  = Droid Serif
    +DRDSERFB = Droid Serif
    +DRDSERFI = Droid Serif
    +DRDSERFZ = Droid Serif
    +LEDGER   = Ledger
    +LEDGERB  = Ledger
    +LEDGERI  = Ledger
    +LEDGERZ  = Ledger
    +NOTOSRF  = Noto Serif
    +NOTOSRFB = Noto Serif
    +NOTOSRFI = Noto Serif
    +NOTOSRFZ = Noto Serif
    +RSLAB    = Roboto Slab
    +RSLABB   = Roboto Slab
    +ATKINSN  = Atkinson Hyperlegible
    +ATKINSNB = Atkinson Hyperlegible
    +ATKINSNI = Atkinson Hyperlegible
    +ATKINSNZ = Atkinson Hyperlegible
    +FIRACOD  = Fira Code
    +FIRACODB = Fira Code
    +JBMONO   = JetBrains Mono
    +JBMONOB  = JetBrains Mono
    +JBMONOI  = JetBrains Mono
    +JBMONOZ  = JetBrains Mono
    +
    +; Display and handwriting faces
    +CNEUE    = Comic Neue
    +CNEUEB   = Comic Neue
    +CNEUEI   = Comic Neue
    +CNEUEZ   = Comic Neue
    +CRELIEF  = Comic Relief
    +CRELIEFB = Comic Relief
    +PATRICK  = Patrick Hand
    +PANGOLN  = Pangolin
    +PACIFIC  = Pacifico
    +ANTON    = Anton
    +BANGERS  = Bangers
    +SPELITE  = Special Elite
    +SANCREK  = Sancreek
    +PMARKER  = Permanent Marker
    +BUNGEE   = Bungee
    +EXO2     = Exo 2
    +EXO2B    = Exo 2
    +EXO2I    = Exo 2
    +EXO2Z    = Exo 2
    +
    +; Pixel faces (their own sizes)
    +FIXED    = Fixed
    +FIXEDB   = Fixed
    +FIXEDI   = Fixed
    +SPLEEN   = Spleen
    +GOHU     = Gohufont
    +GOHUB    = Gohufont
    +COZETTE  = Cozette
    +UNIFONT  = Unifont
    +KIRSCH   = Kirsch
    +DEPARTM  = Departure Mono
    +ARKPIX   = Ark Pixel
    +PIXOP    = Pixel Operator
    +PIXOPB   = Pixel Operator
    +PIXOPM   = Pixel Operator Mono
    +PIXOPMB  = Pixel Operator Mono
    +SILKSCR  = Silkscreen
    +SILKSCRB = Silkscreen
    +TINY5    = Tiny5
    +SWISS    = Swiss
    +SWISSB   = Swiss
    +SWISSI   = Swiss
    +SWISSZ   = Swiss
    +ROMAN    = Roman
    +ROMANB   = Roman
    +ROMANI   = Roman
    +ROMANZ   = Roman
    +SCHOOL   = Schoolbook
    +SCHOOLB  = Schoolbook
    +SCHOOLI  = Schoolbook
    +SCHOOLZ  = Schoolbook
    +TYPEWR   = Typewriter
    +TYPEWRB  = Typewriter
    +TYPEWRI  = Typewriter
    +TYPEWRZ  = Typewriter
    +
    +[licences]
    +DEJAVU   = dejavu/dejavu-fonts-ttf-2.37/LICENSE
    +CROSCORE = croscore/LICENSE
    +CLEARSAN = clearsans/LICENSE.txt
    +GOFONT   = gofont/README
    +DROID    = droidserif/LICENSE
    +LEDGER   = charter/font-bitstream-type1-1.0.4/COPYING x11bitstream/font-bitstream-75dpi-1.0.4/COPYING
    +NOTOSERF = notoserif/OFL.txt
    +RBTSLAB  = robotoslab/LICENSE.txt
    +ATKINSON = atkinson/OFL.txt
    +FIRACODE = firacode/LICENSE
    +JBMONO   = jetbrainsmono/OFL.txt
    +CNEUE    = comicneue/comicneue-master/OFL.txt
    +CRELIEF  = comicrelief/OFL.txt
    +PATRICK  = patrickhand/OFL.txt
    +PANGOLIN = pangolin/OFL.txt
    +PACIFICO = pacifico/OFL.txt
    +ANTON    = anton/OFL.txt
    +BANGERS  = bangers/OFL.txt
    +EXO2     = exo2/OFL.txt
    +SPELITE  = specialelite/LICENSE.txt
    +SANCREEK = sancreek/OFL.txt
    +PMARKER  = permanentmarker/LICENSE.txt
    +BUNGEE   = bungee/OFL.txt
    +MISCFIX  = miscfixed/font-misc-misc-1.1.3/COPYING
    +SPLEEN   = spleen/spleen-2.2.0/LICENSE
    +GOHU     = gohufont/gohufont-2.1/COPYING-LICENSE
    +COZETTE  = cozette/LICENSE
    +UNIFONT  = unifont/OFL-1.1.txt
    +KIRSCH   = kirsch/LICENSE
    +DEPARTUR = departuremono/DepartureMono-1.500/LICENSE
    +ARKPIXEL = arkpixel/OFL.txt
    +PIXOP    = pixeloperator/LICENSE.txt
    +X11ADOBE = x11adobe/font-adobe-75dpi-1.0.4/COPYING
    +SILKSCR  = silkscreen/OFL.txt
    +TINY5    = tiny5/OFL.txt
    +
    +[DJVSANS]
    +set      = base
    +style    = regular
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +smooth   = 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[DJVSANSB]
    +set      = base
    +style    = bold
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans-Bold.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +smooth   = 12 13 14 15 16 18 20 24
    +hinting  = native
    +graycut  = 8
    +
    +[DJVSANSI]
    +set      = base
    +style    = italic
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans-Oblique.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +smooth   = 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[DJVSANSZ]
    +set      = base
    +style    = bolditalic
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans-BoldOblique.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +smooth   = 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[DJVSCND]
    +style    = regular
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansCondensed.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[DJVSCNDB]
    +style    = bold
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansCondensed-Bold.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[DJVSCNDI]
    +style    = italic
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansCondensed-Oblique.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[DJVSCNDZ]
    +style    = bolditalic
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansCondensed-BoldOblique.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[DJVMONO]
    +set      = base
    +style    = regular
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +smooth   = 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[DJVMONOB]
    +set      = base
    +style    = bold
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono-Bold.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +smooth   = 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[DJVMONOI]
    +set      = base
    +style    = italic
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono-Oblique.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +smooth   = 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[DJVMONOZ]
    +set      = base
    +style    = bolditalic
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono-BoldOblique.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +smooth   = 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[DJVSERF]
    +set      = base
    +style    = regular
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +smooth   = 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[DJVSERFB]
    +set      = base
    +style    = bold
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif-Bold.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +smooth   = 12 13 14 15 16 18 20 24
    +hinting  = native
    +graycut  = 12 13 14 15 16 18 20 24
    +
    +[DJVSERFI]
    +set      = base
    +style    = italic
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif-Italic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +smooth   = 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[DJVSERFZ]
    +set      = base
    +style    = bolditalic
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif-BoldItalic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +smooth   = 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[DJVRCND]
    +style    = regular
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerifCondensed.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[DJVRCNDB]
    +style    = bold
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerifCondensed-Bold.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[DJVRCNDI]
    +style    = italic
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerifCondensed-Italic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[DJVRCNDZ]
    +style    = bolditalic
    +licence  = DEJAVU
    +boxdraw  = true
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerifCondensed-BoldItalic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[ARIMO]
    +style    = regular
    +licence  = CROSCORE
    +boxdraw  = true
    +source   = croscore/croscorefonts-1.31.0/Arimo-Regular.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[ARIMOB]
    +style    = bold
    +licence  = CROSCORE
    +boxdraw  = true
    +source   = croscore/croscorefonts-1.31.0/Arimo-Bold.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[ARIMOI]
    +style    = italic
    +licence  = CROSCORE
    +boxdraw  = true
    +source   = croscore/croscorefonts-1.31.0/Arimo-Italic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[ARIMOZ]
    +style    = bolditalic
    +licence  = CROSCORE
    +boxdraw  = true
    +source   = croscore/croscorefonts-1.31.0/Arimo-BoldItalic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[TINOS]
    +style    = regular
    +licence  = CROSCORE
    +boxdraw  = true
    +source   = croscore/croscorefonts-1.31.0/Tinos-Regular.ttf
    +sizes    = 16 18 20 24
    +hinting  = native
    +
    +[TINOSB]
    +style    = bold
    +licence  = CROSCORE
    +boxdraw  = true
    +source   = croscore/croscorefonts-1.31.0/Tinos-Bold.ttf
    +sizes    = 16 18 20 24
    +hinting  = native
    +
    +[TINOSI]
    +style    = italic
    +licence  = CROSCORE
    +boxdraw  = true
    +source   = croscore/croscorefonts-1.31.0/Tinos-Italic.ttf
    +sizes    = 16 18 20 24
    +hinting  = native
    +
    +[TINOSZ]
    +style    = bolditalic
    +licence  = CROSCORE
    +boxdraw  = true
    +source   = croscore/croscorefonts-1.31.0/Tinos-BoldItalic.ttf
    +sizes    = 16 18 20 24
    +hinting  = native
    +
    +[COUSINE]
    +style    = regular
    +licence  = CROSCORE
    +boxdraw  = true
    +source   = croscore/croscorefonts-1.31.0/Cousine-Regular.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[COUSINEB]
    +style    = bold
    +licence  = CROSCORE
    +boxdraw  = true
    +source   = croscore/croscorefonts-1.31.0/Cousine-Bold.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[COUSINEI]
    +style    = italic
    +licence  = CROSCORE
    +boxdraw  = true
    +source   = croscore/croscorefonts-1.31.0/Cousine-Italic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[COUSINEZ]
    +style    = bolditalic
    +licence  = CROSCORE
    +boxdraw  = true
    +source   = croscore/croscorefonts-1.31.0/Cousine-BoldItalic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[CLRSANS]
    +style    = regular
    +licence  = CLEARSAN
    +boxdraw  = true
    +source   = clearsans/ClearSans-Regular.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[CLRSANSB]
    +style    = bold
    +licence  = CLEARSAN
    +boxdraw  = true
    +source   = clearsans/ClearSans-Bold.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[CLRSANSI]
    +style    = italic
    +licence  = CLEARSAN
    +boxdraw  = true
    +source   = clearsans/ClearSans-Italic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[CLRSANSZ]
    +style    = bolditalic
    +licence  = CLEARSAN
    +boxdraw  = true
    +source   = clearsans/ClearSans-BoldItalic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[GO]
    +style    = regular
    +licence  = GOFONT
    +boxdraw  = true
    +source   = gofont/Go-Regular.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[GOB]
    +style    = bold
    +licence  = GOFONT
    +boxdraw  = true
    +source   = gofont/Go-Bold.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[GOI]
    +style    = italic
    +licence  = GOFONT
    +boxdraw  = true
    +source   = gofont/Go-Italic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[GOZ]
    +style    = bolditalic
    +licence  = GOFONT
    +boxdraw  = true
    +source   = gofont/Go-Bold-Italic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[GOMONO]
    +style    = regular
    +licence  = GOFONT
    +boxdraw  = true
    +source   = gofont/Go-Mono.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[GOMONOB]
    +style    = bold
    +licence  = GOFONT
    +boxdraw  = true
    +source   = gofont/Go-Mono-Bold.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[GOMONOI]
    +style    = italic
    +licence  = GOFONT
    +boxdraw  = true
    +source   = gofont/Go-Mono-Italic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[GOMONOZ]
    +style    = bolditalic
    +licence  = GOFONT
    +boxdraw  = true
    +source   = gofont/Go-Mono-Bold-Italic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[DRDSERF]
    +style    = regular
    +licence  = DROID
    +boxdraw  = true
    +source   = droidserif/DroidSerif-Regular.ttf
    +sizes    = 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[DRDSERFB]
    +style    = bold
    +licence  = DROID
    +boxdraw  = true
    +source   = droidserif/DroidSerif-Bold.ttf
    +sizes    = 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[DRDSERFI]
    +style    = italic
    +licence  = DROID
    +boxdraw  = true
    +source   = droidserif/DroidSerif-Italic.ttf
    +sizes    = 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[DRDSERFZ]
    +style    = bolditalic
    +licence  = DROID
    +boxdraw  = true
    +source   = droidserif/DroidSerif-BoldItalic.ttf
    +sizes    = 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[LEDGER]
    +style    = regular
    +licence  = LEDGER
    +boxdraw  = true
    +source   = charter/font-bitstream-type1-1.0.4/c0648bt_.pfb
    +sizes    = 9 11 13 14 16 20
    +hinting  = native
    +source1  = x11bitstream/font-bitstream-75dpi-1.0.4/charR08.bdf
    +sizes1   = native
    +source2  = x11bitstream/font-bitstream-75dpi-1.0.4/charR10.bdf
    +sizes2   = native
    +source3  = x11bitstream/font-bitstream-75dpi-1.0.4/charR12.bdf
    +sizes3   = native
    +source4  = x11bitstream/font-bitstream-75dpi-1.0.4/charR14.bdf
    +sizes4   = native
    +source5  = x11bitstream/font-bitstream-75dpi-1.0.4/charR18.bdf
    +sizes5   = native
    +source6  = x11bitstream/font-bitstream-75dpi-1.0.4/charR24.bdf
    +sizes6   = native
    +
    +[LEDGERB]
    +style    = bold
    +licence  = LEDGER
    +boxdraw  = true
    +source   = charter/font-bitstream-type1-1.0.4/c0632bt_.pfb
    +sizes    = 9 11 13 14 16 20
    +hinting  = native
    +source1  = x11bitstream/font-bitstream-75dpi-1.0.4/charB08.bdf
    +sizes1   = native
    +source2  = x11bitstream/font-bitstream-75dpi-1.0.4/charB10.bdf
    +sizes2   = native
    +source3  = x11bitstream/font-bitstream-75dpi-1.0.4/charB12.bdf
    +sizes3   = native
    +source4  = x11bitstream/font-bitstream-75dpi-1.0.4/charB14.bdf
    +sizes4   = native
    +source5  = x11bitstream/font-bitstream-75dpi-1.0.4/charB18.bdf
    +sizes5   = native
    +source6  = x11bitstream/font-bitstream-75dpi-1.0.4/charB24.bdf
    +sizes6   = native
    +
    +[LEDGERI]
    +style    = italic
    +licence  = LEDGER
    +boxdraw  = true
    +source   = charter/font-bitstream-type1-1.0.4/c0649bt_.pfb
    +sizes    = 9 11 13 14 16 20
    +hinting  = native
    +source1  = x11bitstream/font-bitstream-75dpi-1.0.4/charI08.bdf
    +sizes1   = native
    +source2  = x11bitstream/font-bitstream-75dpi-1.0.4/charI10.bdf
    +sizes2   = native
    +source3  = x11bitstream/font-bitstream-75dpi-1.0.4/charI12.bdf
    +sizes3   = native
    +source4  = x11bitstream/font-bitstream-75dpi-1.0.4/charI14.bdf
    +sizes4   = native
    +source5  = x11bitstream/font-bitstream-75dpi-1.0.4/charI18.bdf
    +sizes5   = native
    +source6  = x11bitstream/font-bitstream-75dpi-1.0.4/charI24.bdf
    +sizes6   = native
    +
    +[LEDGERZ]
    +style    = bolditalic
    +licence  = LEDGER
    +boxdraw  = true
    +source   = charter/font-bitstream-type1-1.0.4/c0633bt_.pfb
    +sizes    = 9 11 13 14 16 20
    +hinting  = native
    +source1  = x11bitstream/font-bitstream-75dpi-1.0.4/charBI08.bdf
    +sizes1   = native
    +source2  = x11bitstream/font-bitstream-75dpi-1.0.4/charBI10.bdf
    +sizes2   = native
    +source3  = x11bitstream/font-bitstream-75dpi-1.0.4/charBI12.bdf
    +sizes3   = native
    +source4  = x11bitstream/font-bitstream-75dpi-1.0.4/charBI14.bdf
    +sizes4   = native
    +source5  = x11bitstream/font-bitstream-75dpi-1.0.4/charBI18.bdf
    +sizes5   = native
    +source6  = x11bitstream/font-bitstream-75dpi-1.0.4/charBI24.bdf
    +sizes6   = native
    +
    +[NOTOSRF]
    +style    = regular
    +licence  = NOTOSERF
    +boxdraw  = true
    +source   = notoserif/NotoSerif-Regular.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[NOTOSRFB]
    +style    = bold
    +licence  = NOTOSERF
    +boxdraw  = true
    +source   = notoserif/NotoSerif-Bold.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[NOTOSRFI]
    +style    = italic
    +licence  = NOTOSERF
    +boxdraw  = true
    +source   = notoserif/NotoSerif-Italic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[NOTOSRFZ]
    +style    = bolditalic
    +licence  = NOTOSERF
    +boxdraw  = true
    +source   = notoserif/NotoSerif-BoldItalic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[RSLAB]
    +style    = regular
    +licence  = RBTSLAB
    +boxdraw  = true
    +source   = robotoslab/RobotoSlab-Regular.ttf
    +sizes    = 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[RSLABB]
    +style    = bold
    +licence  = RBTSLAB
    +boxdraw  = true
    +source   = robotoslab/RobotoSlab-Bold.ttf
    +sizes    = 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = auto
    +
    +[ATKINSN]
    +style    = regular
    +licence  = ATKINSON
    +boxdraw  = true
    +source   = atkinson/AtkinsonHyperlegible-Regular.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[ATKINSNB]
    +style    = bold
    +licence  = ATKINSON
    +boxdraw  = true
    +source   = atkinson/AtkinsonHyperlegible-Bold.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[ATKINSNI]
    +style    = italic
    +licence  = ATKINSON
    +boxdraw  = true
    +source   = atkinson/AtkinsonHyperlegible-Italic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[ATKINSNZ]
    +style    = bolditalic
    +licence  = ATKINSON
    +boxdraw  = true
    +source   = atkinson/AtkinsonHyperlegible-BoldItalic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[FIRACOD]
    +style    = regular
    +licence  = FIRACODE
    +boxdraw  = true
    +source   = firacode/ttf/FiraCode-Regular.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[FIRACODB]
    +style    = bold
    +licence  = FIRACODE
    +boxdraw  = true
    +source   = firacode/ttf/FiraCode-Bold.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[JBMONO]
    +style    = regular
    +licence  = JBMONO
    +boxdraw  = true
    +source   = jetbrainsmono/fonts/ttf/JetBrainsMono-Regular.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[JBMONOB]
    +style    = bold
    +licence  = JBMONO
    +boxdraw  = true
    +source   = jetbrainsmono/fonts/ttf/JetBrainsMono-Bold.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[JBMONOI]
    +style    = italic
    +licence  = JBMONO
    +boxdraw  = true
    +source   = jetbrainsmono/fonts/ttf/JetBrainsMono-Italic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[JBMONOZ]
    +style    = bolditalic
    +licence  = JBMONO
    +boxdraw  = true
    +source   = jetbrainsmono/fonts/ttf/JetBrainsMono-BoldItalic.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +hinting  = native
    +
    +[CNEUE]
    +style    = regular
    +licence  = CNEUE
    +boxdraw  = true
    +source   = comicneue/comicneue-master/Fonts/TTF/ComicNeue/ComicNeue-Regular.ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +
    +[CNEUEB]
    +style    = bold
    +licence  = CNEUE
    +boxdraw  = true
    +source   = comicneue/comicneue-master/Fonts/TTF/ComicNeue/ComicNeue-Bold.ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +
    +[CNEUEI]
    +style    = italic
    +licence  = CNEUE
    +boxdraw  = true
    +source   = comicneue/comicneue-master/Fonts/TTF/ComicNeue/ComicNeue-Italic.ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +
    +[CNEUEZ]
    +style    = bolditalic
    +licence  = CNEUE
    +boxdraw  = true
    +source   = comicneue/comicneue-master/Fonts/TTF/ComicNeue/ComicNeue-BoldItalic.ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +
    +[CRELIEF]
    +style    = regular
    +licence  = CRELIEF
    +boxdraw  = true
    +source   = comicrelief/ComicRelief-Regular.ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +
    +[CRELIEFB]
    +style    = bold
    +licence  = CRELIEF
    +boxdraw  = true
    +source   = comicrelief/ComicRelief-Bold.ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +
    +[PATRICK]
    +style    = regular
    +licence  = PATRICK
    +boxdraw  = true
    +source   = patrickhand/PatrickHand-Regular.ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +
    +[PANGOLN]
    +style    = regular
    +licence  = PANGOLIN
    +boxdraw  = true
    +source   = pangolin/Pangolin-Regular.ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +
    +[PACIFIC]
    +style    = regular
    +licence  = PACIFICO
    +boxdraw  = true
    +source   = pacifico/Pacifico-Regular.ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +
    +[ANTON]
    +style    = regular
    +licence  = ANTON
    +boxdraw  = true
    +source   = anton/Anton-Regular.ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +
    +[BANGERS]
    +style    = regular
    +licence  = BANGERS
    +boxdraw  = true
    +source   = bangers/Bangers-Regular.ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +
    +[SPELITE]
    +style    = regular
    +licence  = SPELITE
    +boxdraw  = true
    +source   = specialelite/SpecialElite-Regular.ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +
    +[SANCREK]
    +style    = regular
    +licence  = SANCREEK
    +boxdraw  = true
    +source   = sancreek/Sancreek-Regular.ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +
    +[PMARKER]
    +style    = regular
    +licence  = PMARKER
    +boxdraw  = true
    +source   = permanentmarker/PermanentMarker-Regular.ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +
    +[BUNGEE]
    +style    = regular
    +licence  = BUNGEE
    +boxdraw  = true
    +source   = bungee/Bungee-Regular.ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +
    +[EXO2]
    +style    = regular
    +licence  = EXO2
    +boxdraw  = true
    +source   = exo2/Exo2[wght].ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +weight   = 400
    +
    +[EXO2B]
    +style    = bold
    +licence  = EXO2
    +boxdraw  = true
    +source   = exo2/Exo2[wght].ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +weight   = 700
    +
    +[EXO2I]
    +style    = italic
    +licence  = EXO2
    +boxdraw  = true
    +source   = exo2/Exo2-Italic[wght].ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +weight   = 400
    +
    +[EXO2Z]
    +style    = bolditalic
    +licence  = EXO2
    +boxdraw  = true
    +source   = exo2/Exo2-Italic[wght].ttf
    +sizes    = 16 18 20 24 28 32 36
    +smooth   = 24 28 32 36
    +hinting  = auto
    +weight   = 700
    +
    +[FIXED]
    +style    = regular
    +licence  = MISCFIX
    +source   = miscfixed/font-misc-misc-1.1.3/4x6.bdf
    +sizes    = native
    +source1  = miscfixed/font-misc-misc-1.1.3/5x7.bdf
    +sizes1   = native
    +source2  = miscfixed/font-misc-misc-1.1.3/5x8.bdf
    +sizes2   = native
    +source3  = miscfixed/font-misc-misc-1.1.3/6x9.bdf
    +sizes3   = native
    +source4  = miscfixed/font-misc-misc-1.1.3/6x10.bdf
    +sizes4   = native
    +source5  = miscfixed/font-misc-misc-1.1.3/6x12.bdf
    +sizes5   = native
    +source6  = miscfixed/font-misc-misc-1.1.3/6x13.bdf
    +sizes6   = native
    +source7  = miscfixed/font-misc-misc-1.1.3/7x14.bdf
    +sizes7   = native
    +source8  = miscfixed/font-misc-misc-1.1.3/9x15.bdf
    +sizes8   = native
    +source9  = miscfixed/font-misc-misc-1.1.3/9x18.bdf
    +sizes9   = native
    +source10 = miscfixed/font-misc-misc-1.1.3/10x20.bdf
    +sizes10  = native
    +
    +[FIXEDB]
    +style    = bold
    +licence  = MISCFIX
    +source   = miscfixed/font-misc-misc-1.1.3/6x13B.bdf
    +sizes    = native
    +source1  = miscfixed/font-misc-misc-1.1.3/7x14B.bdf
    +sizes1   = native
    +source2  = miscfixed/font-misc-misc-1.1.3/9x15B.bdf
    +sizes2   = native
    +source3  = miscfixed/font-misc-misc-1.1.3/9x18B.bdf
    +sizes3   = native
    +
    +[FIXEDI]
    +style    = italic
    +licence  = MISCFIX
    +source   = miscfixed/font-misc-misc-1.1.3/6x13O.bdf
    +sizes    = native
    +
    +[SPLEEN]
    +style    = regular
    +licence  = SPLEEN
    +source   = spleen/spleen-2.2.0/spleen-5x8.bdf
    +sizes    = native
    +source1  = spleen/spleen-2.2.0/spleen-6x12.bdf
    +sizes1   = native
    +source2  = spleen/spleen-2.2.0/cp437/spleen-8x16-ibm-437.bdf
    +sizes2   = native
    +encoding2 = cp437
    +source3  = spleen/spleen-2.2.0/spleen-12x24.bdf
    +sizes3   = native
    +source4  = spleen/spleen-2.2.0/cp437/spleen-16x32-ibm-437.bdf
    +sizes4   = native
    +encoding4 = cp437
    +source5  = spleen/spleen-2.2.0/cp437/spleen-32x64-ibm-437.bdf
    +sizes5   = native
    +encoding5 = cp437
    +
    +[GOHU]
    +style    = regular
    +licence  = GOHU
    +source   = gohufont/gohufont-2.1/gohufont-uni-11.pcf.gz
    +sizes    = native
    +source1  = gohufont/gohufont-2.1/gohufont-uni-14.pcf.gz
    +sizes1   = native
    +
    +[GOHUB]
    +style    = bold
    +licence  = GOHU
    +source   = gohufont/gohufont-2.1/gohufont-uni-11b.pcf.gz
    +sizes    = native
    +source1  = gohufont/gohufont-2.1/gohufont-uni-14b.pcf.gz
    +sizes1   = native
    +
    +[COZETTE]
    +style    = regular
    +licence  = COZETTE
    +source   = cozette/cozette.bdf
    +sizes    = native
    +
    +[UNIFONT]
    +style    = regular
    +licence  = UNIFONT
    +source   = unifont/unifont-18.0.01.bdf
    +sizes    = native
    +
    +[KIRSCH]
    +style    = regular
    +licence  = KIRSCH
    +source   = kirsch/kirsch.bdf
    +sizes    = native
    +
    +[DEPARTM]
    +style    = regular
    +licence  = DEPARTUR
    +source   = departuremono/DepartureMono-1.500/DepartureMono-Regular.otf
    +sizes    = 11 22
    +hinting  = none
    +
    +[ARKPIX]
    +style    = regular
    +licence  = ARKPIXEL
    +source   = arkpixel/ark-pixel-10px-proportional-latin.bdf
    +sizes    = native
    +source1  = arkpixel/ark-pixel-12px-proportional-latin.bdf
    +sizes1   = native
    +
    +[PIXOP]
    +style    = regular
    +licence  = PIXOP
    +source   = pixeloperator/PixelOperator.ttf
    +sizes    = 16 32
    +hinting  = none
    +source1  = pixeloperator/PixelOperator8.ttf
    +sizes1   = 8
    +hinting1 = none
    +
    +[PIXOPB]
    +style    = bold
    +licence  = PIXOP
    +source   = pixeloperator/PixelOperator-Bold.ttf
    +sizes    = 16 32
    +hinting  = none
    +source1  = pixeloperator/PixelOperator8-Bold.ttf
    +sizes1   = 8
    +hinting1 = none
    +
    +[PIXOPM]
    +style    = regular
    +licence  = PIXOP
    +source   = pixeloperator/PixelOperatorMono.ttf
    +sizes    = 16 32
    +hinting  = none
    +source1  = pixeloperator/PixelOperatorMono8.ttf
    +sizes1   = 8
    +hinting1 = none
    +
    +[PIXOPMB]
    +style    = bold
    +licence  = PIXOP
    +source   = pixeloperator/PixelOperatorMono-Bold.ttf
    +sizes    = 16 32
    +hinting  = none
    +source1  = pixeloperator/PixelOperatorMono8-Bold.ttf
    +sizes1   = 8
    +hinting1 = none
    +
    +[SILKSCR]
    +style    = regular
    +licence  = SILKSCR
    +source   = silkscreen/Silkscreen-Regular.ttf
    +sizes    = 8 16
    +hinting  = none
    +
    +[SILKSCRB]
    +style    = bold
    +licence  = SILKSCR
    +source   = silkscreen/Silkscreen-Bold.ttf
    +sizes    = 8 16
    +hinting  = none
    +
    +[TINY5]
    +style    = regular
    +licence  = TINY5
    +source   = tiny5/Tiny5[BLED,JITT,ROND,wdth,wght].ttf
    +sizes    = 8 16
    +hinting  = none
    +
    +[SWISS]
    +style    = regular
    +licence  = X11ADOBE
    +source   = x11adobe/font-adobe-75dpi-1.0.4/helvR08.bdf
    +sizes    = native
    +source1  = x11adobe/font-adobe-75dpi-1.0.4/helvR10.bdf
    +sizes1   = native
    +source2  = x11adobe/font-adobe-75dpi-1.0.4/helvR12.bdf
    +sizes2   = native
    +source3  = x11adobe/font-adobe-75dpi-1.0.4/helvR14.bdf
    +sizes3   = native
    +source4  = x11adobe/font-adobe-75dpi-1.0.4/helvR18.bdf
    +sizes4   = native
    +source5  = x11adobe/font-adobe-75dpi-1.0.4/helvR24.bdf
    +sizes5   = native
    +source6  = x11adobe/font-adobe-100dpi-1.0.4/helvR08.bdf
    +sizes6   = native
    +source7  = x11adobe/font-adobe-100dpi-1.0.4/helvR12.bdf
    +sizes7   = native
    +source8  = x11adobe/font-adobe-100dpi-1.0.4/helvR14.bdf
    +sizes8   = native
    +source9  = x11adobe/font-adobe-100dpi-1.0.4/helvR18.bdf
    +sizes9   = native
    +source10 = x11adobe/font-adobe-100dpi-1.0.4/helvR24.bdf
    +sizes10  = native
    +
    +[SWISSB]
    +style    = bold
    +licence  = X11ADOBE
    +source   = x11adobe/font-adobe-75dpi-1.0.4/helvB08.bdf
    +sizes    = native
    +source1  = x11adobe/font-adobe-75dpi-1.0.4/helvB10.bdf
    +sizes1   = native
    +source2  = x11adobe/font-adobe-75dpi-1.0.4/helvB12.bdf
    +sizes2   = native
    +source3  = x11adobe/font-adobe-75dpi-1.0.4/helvB14.bdf
    +sizes3   = native
    +source4  = x11adobe/font-adobe-75dpi-1.0.4/helvB18.bdf
    +sizes4   = native
    +source5  = x11adobe/font-adobe-75dpi-1.0.4/helvB24.bdf
    +sizes5   = native
    +source6  = x11adobe/font-adobe-100dpi-1.0.4/helvB08.bdf
    +sizes6   = native
    +source7  = x11adobe/font-adobe-100dpi-1.0.4/helvB12.bdf
    +sizes7   = native
    +source8  = x11adobe/font-adobe-100dpi-1.0.4/helvB14.bdf
    +sizes8   = native
    +source9  = x11adobe/font-adobe-100dpi-1.0.4/helvB18.bdf
    +sizes9   = native
    +source10 = x11adobe/font-adobe-100dpi-1.0.4/helvB24.bdf
    +sizes10  = native
    +
    +[SWISSI]
    +style    = italic
    +licence  = X11ADOBE
    +source   = x11adobe/font-adobe-75dpi-1.0.4/helvO08.bdf
    +sizes    = native
    +source1  = x11adobe/font-adobe-75dpi-1.0.4/helvO10.bdf
    +sizes1   = native
    +source2  = x11adobe/font-adobe-75dpi-1.0.4/helvO12.bdf
    +sizes2   = native
    +source3  = x11adobe/font-adobe-75dpi-1.0.4/helvO14.bdf
    +sizes3   = native
    +source4  = x11adobe/font-adobe-75dpi-1.0.4/helvO18.bdf
    +sizes4   = native
    +source5  = x11adobe/font-adobe-75dpi-1.0.4/helvO24.bdf
    +sizes5   = native
    +source6  = x11adobe/font-adobe-100dpi-1.0.4/helvO08.bdf
    +sizes6   = native
    +source7  = x11adobe/font-adobe-100dpi-1.0.4/helvO12.bdf
    +sizes7   = native
    +source8  = x11adobe/font-adobe-100dpi-1.0.4/helvO14.bdf
    +sizes8   = native
    +source9  = x11adobe/font-adobe-100dpi-1.0.4/helvO18.bdf
    +sizes9   = native
    +source10 = x11adobe/font-adobe-100dpi-1.0.4/helvO24.bdf
    +sizes10  = native
    +
    +[SWISSZ]
    +style    = bolditalic
    +licence  = X11ADOBE
    +source   = x11adobe/font-adobe-75dpi-1.0.4/helvBO08.bdf
    +sizes    = native
    +source1  = x11adobe/font-adobe-75dpi-1.0.4/helvBO10.bdf
    +sizes1   = native
    +source2  = x11adobe/font-adobe-75dpi-1.0.4/helvBO12.bdf
    +sizes2   = native
    +source3  = x11adobe/font-adobe-75dpi-1.0.4/helvBO14.bdf
    +sizes3   = native
    +source4  = x11adobe/font-adobe-75dpi-1.0.4/helvBO18.bdf
    +sizes4   = native
    +source5  = x11adobe/font-adobe-75dpi-1.0.4/helvBO24.bdf
    +sizes5   = native
    +source6  = x11adobe/font-adobe-100dpi-1.0.4/helvBO08.bdf
    +sizes6   = native
    +source7  = x11adobe/font-adobe-100dpi-1.0.4/helvBO12.bdf
    +sizes7   = native
    +source8  = x11adobe/font-adobe-100dpi-1.0.4/helvBO14.bdf
    +sizes8   = native
    +source9  = x11adobe/font-adobe-100dpi-1.0.4/helvBO18.bdf
    +sizes9   = native
    +source10 = x11adobe/font-adobe-100dpi-1.0.4/helvBO24.bdf
    +sizes10  = native
    +
    +[ROMAN]
    +style    = regular
    +licence  = X11ADOBE
    +source   = x11adobe/font-adobe-75dpi-1.0.4/timR08.bdf
    +sizes    = native
    +source1  = x11adobe/font-adobe-75dpi-1.0.4/timR10.bdf
    +sizes1   = native
    +source2  = x11adobe/font-adobe-75dpi-1.0.4/timR12.bdf
    +sizes2   = native
    +source3  = x11adobe/font-adobe-75dpi-1.0.4/timR14.bdf
    +sizes3   = native
    +source4  = x11adobe/font-adobe-75dpi-1.0.4/timR18.bdf
    +sizes4   = native
    +source5  = x11adobe/font-adobe-75dpi-1.0.4/timR24.bdf
    +sizes5   = native
    +source6  = x11adobe/font-adobe-100dpi-1.0.4/timR08.bdf
    +sizes6   = native
    +source7  = x11adobe/font-adobe-100dpi-1.0.4/timR12.bdf
    +sizes7   = native
    +source8  = x11adobe/font-adobe-100dpi-1.0.4/timR14.bdf
    +sizes8   = native
    +source9  = x11adobe/font-adobe-100dpi-1.0.4/timR18.bdf
    +sizes9   = native
    +source10 = x11adobe/font-adobe-100dpi-1.0.4/timR24.bdf
    +sizes10  = native
    +
    +[ROMANB]
    +style    = bold
    +licence  = X11ADOBE
    +source   = x11adobe/font-adobe-75dpi-1.0.4/timB08.bdf
    +sizes    = native
    +source1  = x11adobe/font-adobe-75dpi-1.0.4/timB10.bdf
    +sizes1   = native
    +source2  = x11adobe/font-adobe-75dpi-1.0.4/timB12.bdf
    +sizes2   = native
    +source3  = x11adobe/font-adobe-75dpi-1.0.4/timB14.bdf
    +sizes3   = native
    +source4  = x11adobe/font-adobe-75dpi-1.0.4/timB18.bdf
    +sizes4   = native
    +source5  = x11adobe/font-adobe-75dpi-1.0.4/timB24.bdf
    +sizes5   = native
    +source6  = x11adobe/font-adobe-100dpi-1.0.4/timB08.bdf
    +sizes6   = native
    +source7  = x11adobe/font-adobe-100dpi-1.0.4/timB12.bdf
    +sizes7   = native
    +source8  = x11adobe/font-adobe-100dpi-1.0.4/timB14.bdf
    +sizes8   = native
    +source9  = x11adobe/font-adobe-100dpi-1.0.4/timB18.bdf
    +sizes9   = native
    +source10 = x11adobe/font-adobe-100dpi-1.0.4/timB24.bdf
    +sizes10  = native
    +
    +[ROMANI]
    +style    = italic
    +licence  = X11ADOBE
    +source   = x11adobe/font-adobe-75dpi-1.0.4/timI08.bdf
    +sizes    = native
    +source1  = x11adobe/font-adobe-75dpi-1.0.4/timI10.bdf
    +sizes1   = native
    +source2  = x11adobe/font-adobe-75dpi-1.0.4/timI12.bdf
    +sizes2   = native
    +source3  = x11adobe/font-adobe-75dpi-1.0.4/timI14.bdf
    +sizes3   = native
    +source4  = x11adobe/font-adobe-75dpi-1.0.4/timI18.bdf
    +sizes4   = native
    +source5  = x11adobe/font-adobe-75dpi-1.0.4/timI24.bdf
    +sizes5   = native
    +source6  = x11adobe/font-adobe-100dpi-1.0.4/timI08.bdf
    +sizes6   = native
    +source7  = x11adobe/font-adobe-100dpi-1.0.4/timI12.bdf
    +sizes7   = native
    +source8  = x11adobe/font-adobe-100dpi-1.0.4/timI14.bdf
    +sizes8   = native
    +source9  = x11adobe/font-adobe-100dpi-1.0.4/timI18.bdf
    +sizes9   = native
    +source10 = x11adobe/font-adobe-100dpi-1.0.4/timI24.bdf
    +sizes10  = native
    +
    +[ROMANZ]
    +style    = bolditalic
    +licence  = X11ADOBE
    +source   = x11adobe/font-adobe-75dpi-1.0.4/timBI08.bdf
    +sizes    = native
    +source1  = x11adobe/font-adobe-75dpi-1.0.4/timBI10.bdf
    +sizes1   = native
    +source2  = x11adobe/font-adobe-75dpi-1.0.4/timBI12.bdf
    +sizes2   = native
    +source3  = x11adobe/font-adobe-75dpi-1.0.4/timBI14.bdf
    +sizes3   = native
    +source4  = x11adobe/font-adobe-75dpi-1.0.4/timBI18.bdf
    +sizes4   = native
    +source5  = x11adobe/font-adobe-75dpi-1.0.4/timBI24.bdf
    +sizes5   = native
    +source6  = x11adobe/font-adobe-100dpi-1.0.4/timBI08.bdf
    +sizes6   = native
    +source7  = x11adobe/font-adobe-100dpi-1.0.4/timBI12.bdf
    +sizes7   = native
    +source8  = x11adobe/font-adobe-100dpi-1.0.4/timBI14.bdf
    +sizes8   = native
    +source9  = x11adobe/font-adobe-100dpi-1.0.4/timBI18.bdf
    +sizes9   = native
    +source10 = x11adobe/font-adobe-100dpi-1.0.4/timBI24.bdf
    +sizes10  = native
    +
    +[SCHOOL]
    +style    = regular
    +licence  = X11ADOBE
    +source   = x11adobe/font-adobe-75dpi-1.0.4/ncenR08.bdf
    +sizes    = native
    +source1  = x11adobe/font-adobe-75dpi-1.0.4/ncenR10.bdf
    +sizes1   = native
    +source2  = x11adobe/font-adobe-75dpi-1.0.4/ncenR12.bdf
    +sizes2   = native
    +source3  = x11adobe/font-adobe-75dpi-1.0.4/ncenR14.bdf
    +sizes3   = native
    +source4  = x11adobe/font-adobe-75dpi-1.0.4/ncenR18.bdf
    +sizes4   = native
    +source5  = x11adobe/font-adobe-75dpi-1.0.4/ncenR24.bdf
    +sizes5   = native
    +source6  = x11adobe/font-adobe-100dpi-1.0.4/ncenR08.bdf
    +sizes6   = native
    +source7  = x11adobe/font-adobe-100dpi-1.0.4/ncenR12.bdf
    +sizes7   = native
    +source8  = x11adobe/font-adobe-100dpi-1.0.4/ncenR14.bdf
    +sizes8   = native
    +source9  = x11adobe/font-adobe-100dpi-1.0.4/ncenR18.bdf
    +sizes9   = native
    +source10 = x11adobe/font-adobe-100dpi-1.0.4/ncenR24.bdf
    +sizes10  = native
    +
    +[SCHOOLB]
    +style    = bold
    +licence  = X11ADOBE
    +source   = x11adobe/font-adobe-75dpi-1.0.4/ncenB08.bdf
    +sizes    = native
    +source1  = x11adobe/font-adobe-75dpi-1.0.4/ncenB10.bdf
    +sizes1   = native
    +source2  = x11adobe/font-adobe-75dpi-1.0.4/ncenB12.bdf
    +sizes2   = native
    +source3  = x11adobe/font-adobe-75dpi-1.0.4/ncenB14.bdf
    +sizes3   = native
    +source4  = x11adobe/font-adobe-75dpi-1.0.4/ncenB18.bdf
    +sizes4   = native
    +source5  = x11adobe/font-adobe-75dpi-1.0.4/ncenB24.bdf
    +sizes5   = native
    +source6  = x11adobe/font-adobe-100dpi-1.0.4/ncenB08.bdf
    +sizes6   = native
    +source7  = x11adobe/font-adobe-100dpi-1.0.4/ncenB12.bdf
    +sizes7   = native
    +source8  = x11adobe/font-adobe-100dpi-1.0.4/ncenB14.bdf
    +sizes8   = native
    +source9  = x11adobe/font-adobe-100dpi-1.0.4/ncenB18.bdf
    +sizes9   = native
    +source10 = x11adobe/font-adobe-100dpi-1.0.4/ncenB24.bdf
    +sizes10  = native
    +
    +[SCHOOLI]
    +style    = italic
    +licence  = X11ADOBE
    +source   = x11adobe/font-adobe-75dpi-1.0.4/ncenI08.bdf
    +sizes    = native
    +source1  = x11adobe/font-adobe-75dpi-1.0.4/ncenI10.bdf
    +sizes1   = native
    +source2  = x11adobe/font-adobe-75dpi-1.0.4/ncenI12.bdf
    +sizes2   = native
    +source3  = x11adobe/font-adobe-75dpi-1.0.4/ncenI14.bdf
    +sizes3   = native
    +source4  = x11adobe/font-adobe-75dpi-1.0.4/ncenI18.bdf
    +sizes4   = native
    +source5  = x11adobe/font-adobe-75dpi-1.0.4/ncenI24.bdf
    +sizes5   = native
    +source6  = x11adobe/font-adobe-100dpi-1.0.4/ncenI08.bdf
    +sizes6   = native
    +source7  = x11adobe/font-adobe-100dpi-1.0.4/ncenI12.bdf
    +sizes7   = native
    +source8  = x11adobe/font-adobe-100dpi-1.0.4/ncenI14.bdf
    +sizes8   = native
    +source9  = x11adobe/font-adobe-100dpi-1.0.4/ncenI18.bdf
    +sizes9   = native
    +source10 = x11adobe/font-adobe-100dpi-1.0.4/ncenI24.bdf
    +sizes10  = native
    +
    +[SCHOOLZ]
    +style    = bolditalic
    +licence  = X11ADOBE
    +source   = x11adobe/font-adobe-75dpi-1.0.4/ncenBI08.bdf
    +sizes    = native
    +source1  = x11adobe/font-adobe-75dpi-1.0.4/ncenBI10.bdf
    +sizes1   = native
    +source2  = x11adobe/font-adobe-75dpi-1.0.4/ncenBI12.bdf
    +sizes2   = native
    +source3  = x11adobe/font-adobe-75dpi-1.0.4/ncenBI14.bdf
    +sizes3   = native
    +source4  = x11adobe/font-adobe-75dpi-1.0.4/ncenBI18.bdf
    +sizes4   = native
    +source5  = x11adobe/font-adobe-75dpi-1.0.4/ncenBI24.bdf
    +sizes5   = native
    +source6  = x11adobe/font-adobe-100dpi-1.0.4/ncenBI08.bdf
    +sizes6   = native
    +source7  = x11adobe/font-adobe-100dpi-1.0.4/ncenBI12.bdf
    +sizes7   = native
    +source8  = x11adobe/font-adobe-100dpi-1.0.4/ncenBI14.bdf
    +sizes8   = native
    +source9  = x11adobe/font-adobe-100dpi-1.0.4/ncenBI18.bdf
    +sizes9   = native
    +source10 = x11adobe/font-adobe-100dpi-1.0.4/ncenBI24.bdf
    +sizes10  = native
    +
    +[TYPEWR]
    +style    = regular
    +licence  = X11ADOBE
    +source   = x11adobe/font-adobe-75dpi-1.0.4/courR08.bdf
    +sizes    = native
    +source1  = x11adobe/font-adobe-75dpi-1.0.4/courR10.bdf
    +sizes1   = native
    +source2  = x11adobe/font-adobe-75dpi-1.0.4/courR12.bdf
    +sizes2   = native
    +source3  = x11adobe/font-adobe-75dpi-1.0.4/courR14.bdf
    +sizes3   = native
    +source4  = x11adobe/font-adobe-75dpi-1.0.4/courR18.bdf
    +sizes4   = native
    +source5  = x11adobe/font-adobe-75dpi-1.0.4/courR24.bdf
    +sizes5   = native
    +source6  = x11adobe/font-adobe-100dpi-1.0.4/courR08.bdf
    +sizes6   = native
    +source7  = x11adobe/font-adobe-100dpi-1.0.4/courR12.bdf
    +sizes7   = native
    +source8  = x11adobe/font-adobe-100dpi-1.0.4/courR14.bdf
    +sizes8   = native
    +source9  = x11adobe/font-adobe-100dpi-1.0.4/courR18.bdf
    +sizes9   = native
    +source10 = x11adobe/font-adobe-100dpi-1.0.4/courR24.bdf
    +sizes10  = native
    +
    +[TYPEWRB]
    +style    = bold
    +licence  = X11ADOBE
    +source   = x11adobe/font-adobe-75dpi-1.0.4/courB08.bdf
    +sizes    = native
    +source1  = x11adobe/font-adobe-75dpi-1.0.4/courB10.bdf
    +sizes1   = native
    +source2  = x11adobe/font-adobe-75dpi-1.0.4/courB12.bdf
    +sizes2   = native
    +source3  = x11adobe/font-adobe-75dpi-1.0.4/courB14.bdf
    +sizes3   = native
    +source4  = x11adobe/font-adobe-75dpi-1.0.4/courB18.bdf
    +sizes4   = native
    +source5  = x11adobe/font-adobe-75dpi-1.0.4/courB24.bdf
    +sizes5   = native
    +source6  = x11adobe/font-adobe-100dpi-1.0.4/courB08.bdf
    +sizes6   = native
    +source7  = x11adobe/font-adobe-100dpi-1.0.4/courB12.bdf
    +sizes7   = native
    +source8  = x11adobe/font-adobe-100dpi-1.0.4/courB14.bdf
    +sizes8   = native
    +source9  = x11adobe/font-adobe-100dpi-1.0.4/courB18.bdf
    +sizes9   = native
    +source10 = x11adobe/font-adobe-100dpi-1.0.4/courB24.bdf
    +sizes10  = native
    +
    +[TYPEWRI]
    +style    = italic
    +licence  = X11ADOBE
    +source   = x11adobe/font-adobe-75dpi-1.0.4/courO08.bdf
    +sizes    = native
    +source1  = x11adobe/font-adobe-75dpi-1.0.4/courO10.bdf
    +sizes1   = native
    +source2  = x11adobe/font-adobe-75dpi-1.0.4/courO12.bdf
    +sizes2   = native
    +source3  = x11adobe/font-adobe-75dpi-1.0.4/courO14.bdf
    +sizes3   = native
    +source4  = x11adobe/font-adobe-75dpi-1.0.4/courO18.bdf
    +sizes4   = native
    +source5  = x11adobe/font-adobe-75dpi-1.0.4/courO24.bdf
    +sizes5   = native
    +source6  = x11adobe/font-adobe-100dpi-1.0.4/courO08.bdf
    +sizes6   = native
    +source7  = x11adobe/font-adobe-100dpi-1.0.4/courO12.bdf
    +sizes7   = native
    +source8  = x11adobe/font-adobe-100dpi-1.0.4/courO14.bdf
    +sizes8   = native
    +source9  = x11adobe/font-adobe-100dpi-1.0.4/courO18.bdf
    +sizes9   = native
    +source10 = x11adobe/font-adobe-100dpi-1.0.4/courO24.bdf
    +sizes10  = native
    +
    +[TYPEWRZ]
    +style    = bolditalic
    +licence  = X11ADOBE
    +source   = x11adobe/font-adobe-75dpi-1.0.4/courBO08.bdf
    +sizes    = native
    +source1  = x11adobe/font-adobe-75dpi-1.0.4/courBO10.bdf
    +sizes1   = native
    +source2  = x11adobe/font-adobe-75dpi-1.0.4/courBO12.bdf
    +sizes2   = native
    +source3  = x11adobe/font-adobe-75dpi-1.0.4/courBO14.bdf
    +sizes3   = native
    +source4  = x11adobe/font-adobe-75dpi-1.0.4/courBO18.bdf
    +sizes4   = native
    +source5  = x11adobe/font-adobe-75dpi-1.0.4/courBO24.bdf
    +sizes5   = native
    +source6  = x11adobe/font-adobe-100dpi-1.0.4/courBO08.bdf
    +sizes6   = native
    +source7  = x11adobe/font-adobe-100dpi-1.0.4/courBO12.bdf
    +sizes7   = native
    +source8  = x11adobe/font-adobe-100dpi-1.0.4/courBO14.bdf
    +sizes8   = native
    +source9  = x11adobe/font-adobe-100dpi-1.0.4/courBO18.bdf
    +sizes9   = native
    +source10 = x11adobe/font-adobe-100dpi-1.0.4/courBO24.bdf
    +sizes10  = native
    diff --git a/src/fonts/installFonts.sh b/src/fonts/installFonts.sh
    new file mode 100755
    index 00000000..a7bfcd21
    --- /dev/null
    +++ b/src/fonts/installFonts.sh
    @@ -0,0 +1,165 @@
    +#!/bin/bash
    +# The MIT License (MIT)
    +#
    +# Copyright (C) 2026 Scott Duensing
    +#
    +# Permission is hereby granted, free of charge, to any person obtaining a copy
    +# of this software and associated documentation files (the "Software"), to
    +# deal in the Software without restriction, including without limitation the
    +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +# sell copies of the Software, and to permit persons to whom the Software is
    +# furnished to do so, subject to the following conditions:
    +#
    +# The above copyright notice and this permission notice shall be included in
    +# all copies or substantial portions of the Software.
    +#
    +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +# IN THE SOFTWARE.
    +
    +# installFonts.sh [--force] -- the inputs of DVX's fonts (claudeDocs/fonts.md):
    +#
    +#   toolchains/freetype  FreeType, built from its pinned source release as a
    +#                        static library for the font converter (never the
    +#                        system one: converted fonts and the screenshot
    +#                        goldens must not change with the host's FreeType)
    +#   toolchains/fonts     every font of src/fonts/sources.txt, each in
    +#                        toolchains/fonts// with its licence files
    +#
    +# Called by installToolchain.sh; safe to run on its own.  Nothing is
    +# written outside the repository and nothing needs root.  A change to
    +# sources.txt installs the fonts again.
    +
    +set -euo pipefail
    +
    +FREETYPE_VERSION="2.13.3"
    +FREETYPE_FILE="freetype-${FREETYPE_VERSION}.tar.xz"
    +FREETYPE_URL="https://download.savannah.gnu.org/releases/freetype/${FREETYPE_FILE}"
    +FREETYPE_SHA256="0550350666d427c74daeb85d5ac7bb353acba5f76956395995311a9c6f063289"
    +
    +# Only what the converter needs: no compression or shaping libraries
    +# (FreeType's own gzip reader still opens .pcf.gz)
    +FREETYPE_CONFIG="--disable-shared --enable-static --with-zlib=no --with-bzip2=no --with-png=no --with-harfbuzz=no --with-brotli=no"
    +
    +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
    +TOOLCHAINS="${ROOT}/toolchains"
    +CACHE="${TOOLCHAINS}/cache"
    +FONT_CACHE="${CACHE}/fonts"
    +FREETYPE_DIR="${TOOLCHAINS}/freetype"
    +FONTS_DIR="${TOOLCHAINS}/fonts"
    +SOURCES="${ROOT}/src/fonts/sources.txt"
    +PIN_FILE=".installed"
    +FORCE=0
    +
    +if [ "${1:-}" = "--force" ]; then
    +    FORCE=1
    +fi
    +
    +# fetchFile URL FILE SHA256 -- download URL to FILE once, checked
    +fetchFile() {
    +    local url="$1"
    +    local file="$2"
    +    local sum="$3"
    +
    +    if [ -f "${file}" ] && echo "${sum}  ${file}" | sha256sum -c --status; then
    +        return 0
    +    fi
    +
    +    echo "installFonts: downloading ${file##*/}"
    +    mkdir -p "$(dirname "${file}")"
    +    curl -fL --retry 3 -s -o "${file}.part" "${url}"
    +    mv "${file}.part" "${file}"
    +
    +    if ! echo "${sum}  ${file}" | sha256sum -c --status; then
    +        echo "installFonts: checksum mismatch for ${url} (expected ${sum}); refusing to install" >&2
    +        rm -f "${file}"
    +        exit 1
    +    fi
    +}
    +
    +
    +# FreeType
    +if [ "${FORCE}" -eq 0 ] && [ -f "${FREETYPE_DIR}/${PIN_FILE}" ] && [ "$(cat "${FREETYPE_DIR}/${PIN_FILE}")" = "${FREETYPE_SHA256}" ] && [ -f "${FREETYPE_DIR}/lib/libfreetype.a" ]; then
    +    echo "installFonts: FreeType ${FREETYPE_VERSION} already installed"
    +else
    +    fetchFile "${FREETYPE_URL}" "${CACHE}/${FREETYPE_FILE}" "${FREETYPE_SHA256}"
    +    build="${FREETYPE_DIR}.build"
    +    rm -rf "${build}" "${FREETYPE_DIR}"
    +    mkdir -p "${build}"
    +    tar -xJf "${CACHE}/${FREETYPE_FILE}" -C "${build}" --strip-components=1
    +    echo "installFonts: building FreeType ${FREETYPE_VERSION}"
    +
    +    (
    +        cd "${build}"
    +        # shellcheck disable=SC2086
    +        ./configure --prefix="${FREETYPE_DIR}" ${FREETYPE_CONFIG}
    +        make -j"$(nproc)"
    +        make install
    +    ) > "${build}.log" 2>&1 || {
    +        echo "installFonts: FreeType build failed; see ${build}.log" >&2
    +        exit 1
    +    }
    +
    +    rm -rf "${build}" "${build}.log"
    +    echo "${FREETYPE_SHA256}" > "${FREETYPE_DIR}/${PIN_FILE}"
    +    echo "installFonts: FreeType installed in ${FREETYPE_DIR}"
    +fi
    +
    +
    +# Fonts: the pin is the source list itself
    +fontsPin="$(sha256sum "${SOURCES}" | cut -d' ' -f1)"
    +
    +if [ "${FORCE}" -eq 0 ] && [ -f "${FONTS_DIR}/${PIN_FILE}" ] && [ "$(cat "${FONTS_DIR}/${PIN_FILE}")" = "${fontsPin}" ]; then
    +    echo "installFonts: fonts already installed"
    +    exit 0
    +fi
    +
    +rm -rf "${FONTS_DIR}.new"
    +mkdir -p "${FONTS_DIR}.new"
    +
    +while read -r id kind sum url name; do
    +    case "${id}" in
    +        ""|"#"*) continue ;;
    +    esac
    +
    +    name="${name:-$(python3 -c 'import sys, urllib.parse; print(urllib.parse.unquote(sys.argv[1]))' "${url%%\?*}")}"
    +    name="${name##*/}"
    +    dest="${FONTS_DIR}.new/${id}"
    +    mkdir -p "${dest}"
    +
    +    if [ "${kind}" = "local" ]; then
    +        src="${ROOT}/${url}"
    +
    +        if ! echo "${sum}  ${src}" | sha256sum -c --status; then
    +            echo "installFonts: ${url} does not match its sha256 in sources.txt" >&2
    +            exit 1
    +        fi
    +
    +        cp "${src}" "${dest}/${name}"
    +        continue
    +    fi
    +
    +    file="${FONT_CACHE}/${id}/${name}"
    +    fetchFile "${url}" "${file}" "${sum}"
    +
    +    case "${kind}" in
    +        file) cp "${file}" "${dest}/${name}" ;;
    +        zip)  unzip -q -o "${file}" -d "${dest}" ;;
    +        tar)  tar -xf "${file}" -C "${dest}" ;;
    +        gz)   gunzip -c "${file}" > "${dest}/${name%.gz}" ;;
    +        b64)  base64 -d "${file}" > "${dest}/${name}" ;;
    +        *)
    +            echo "installFonts: unknown kind '${kind}' for ${id} in sources.txt" >&2
    +            exit 1
    +            ;;
    +    esac
    +done < "${SOURCES}"
    +
    +rm -rf "${FONTS_DIR}"
    +mv "${FONTS_DIR}.new" "${FONTS_DIR}"
    +echo "${fontsPin}" > "${FONTS_DIR}/${PIN_FILE}"
    +echo "installFonts: $(find "${FONTS_DIR}" -mindepth 1 -maxdepth 1 -type d | wc -l) fonts installed in ${FONTS_DIR}"
    diff --git a/src/fonts/licenses/Apache-2.0.txt b/src/fonts/licenses/Apache-2.0.txt
    new file mode 100644
    index 00000000..d6456956
    --- /dev/null
    +++ b/src/fonts/licenses/Apache-2.0.txt
    @@ -0,0 +1,202 @@
    +
    +                                 Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +
    +   1. Definitions.
    +
    +      "License" shall mean the terms and conditions for use, reproduction,
    +      and distribution as defined by Sections 1 through 9 of this document.
    +
    +      "Licensor" shall mean the copyright owner or entity authorized by
    +      the copyright owner that is granting the License.
    +
    +      "Legal Entity" shall mean the union of the acting entity and all
    +      other entities that control, are controlled by, or are under common
    +      control with that entity. For the purposes of this definition,
    +      "control" means (i) the power, direct or indirect, to cause the
    +      direction or management of such entity, whether by contract or
    +      otherwise, or (ii) ownership of fifty percent (50%) or more of the
    +      outstanding shares, or (iii) beneficial ownership of such entity.
    +
    +      "You" (or "Your") shall mean an individual or Legal Entity
    +      exercising permissions granted by this License.
    +
    +      "Source" form shall mean the preferred form for making modifications,
    +      including but not limited to software source code, documentation
    +      source, and configuration files.
    +
    +      "Object" form shall mean any form resulting from mechanical
    +      transformation or translation of a Source form, including but
    +      not limited to compiled object code, generated documentation,
    +      and conversions to other media types.
    +
    +      "Work" shall mean the work of authorship, whether in Source or
    +      Object form, made available under the License, as indicated by a
    +      copyright notice that is included in or attached to the work
    +      (an example is provided in the Appendix below).
    +
    +      "Derivative Works" shall mean any work, whether in Source or Object
    +      form, that is based on (or derived from) the Work and for which the
    +      editorial revisions, annotations, elaborations, or other modifications
    +      represent, as a whole, an original work of authorship. For the purposes
    +      of this License, Derivative Works shall not include works that remain
    +      separable from, or merely link (or bind by name) to the interfaces of,
    +      the Work and Derivative Works thereof.
    +
    +      "Contribution" shall mean any work of authorship, including
    +      the original version of the Work and any modifications or additions
    +      to that Work or Derivative Works thereof, that is intentionally
    +      submitted to Licensor for inclusion in the Work by the copyright owner
    +      or by an individual or Legal Entity authorized to submit on behalf of
    +      the copyright owner. For the purposes of this definition, "submitted"
    +      means any form of electronic, verbal, or written communication sent
    +      to the Licensor or its representatives, including but not limited to
    +      communication on electronic mailing lists, source code control systems,
    +      and issue tracking systems that are managed by, or on behalf of, the
    +      Licensor for the purpose of discussing and improving the Work, but
    +      excluding communication that is conspicuously marked or otherwise
    +      designated in writing by the copyright owner as "Not a Contribution."
    +
    +      "Contributor" shall mean Licensor and any individual or Legal Entity
    +      on behalf of whom a Contribution has been received by Licensor and
    +      subsequently incorporated within the Work.
    +
    +   2. Grant of Copyright License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      copyright license to reproduce, prepare Derivative Works of,
    +      publicly display, publicly perform, sublicense, and distribute the
    +      Work and such Derivative Works in Source or Object form.
    +
    +   3. Grant of Patent License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      (except as stated in this section) patent license to make, have made,
    +      use, offer to sell, sell, import, and otherwise transfer the Work,
    +      where such license applies only to those patent claims licensable
    +      by such Contributor that are necessarily infringed by their
    +      Contribution(s) alone or by combination of their Contribution(s)
    +      with the Work to which such Contribution(s) was submitted. If You
    +      institute patent litigation against any entity (including a
    +      cross-claim or counterclaim in a lawsuit) alleging that the Work
    +      or a Contribution incorporated within the Work constitutes direct
    +      or contributory patent infringement, then any patent licenses
    +      granted to You under this License for that Work shall terminate
    +      as of the date such litigation is filed.
    +
    +   4. Redistribution. You may reproduce and distribute copies of the
    +      Work or Derivative Works thereof in any medium, with or without
    +      modifications, and in Source or Object form, provided that You
    +      meet the following conditions:
    +
    +      (a) You must give any other recipients of the Work or
    +          Derivative Works a copy of this License; and
    +
    +      (b) You must cause any modified files to carry prominent notices
    +          stating that You changed the files; and
    +
    +      (c) You must retain, in the Source form of any Derivative Works
    +          that You distribute, all copyright, patent, trademark, and
    +          attribution notices from the Source form of the Work,
    +          excluding those notices that do not pertain to any part of
    +          the Derivative Works; and
    +
    +      (d) If the Work includes a "NOTICE" text file as part of its
    +          distribution, then any Derivative Works that You distribute must
    +          include a readable copy of the attribution notices contained
    +          within such NOTICE file, excluding those notices that do not
    +          pertain to any part of the Derivative Works, in at least one
    +          of the following places: within a NOTICE text file distributed
    +          as part of the Derivative Works; within the Source form or
    +          documentation, if provided along with the Derivative Works; or,
    +          within a display generated by the Derivative Works, if and
    +          wherever such third-party notices normally appear. The contents
    +          of the NOTICE file are for informational purposes only and
    +          do not modify the License. You may add Your own attribution
    +          notices within Derivative Works that You distribute, alongside
    +          or as an addendum to the NOTICE text from the Work, provided
    +          that such additional attribution notices cannot be construed
    +          as modifying the License.
    +
    +      You may add Your own copyright statement to Your modifications and
    +      may provide additional or different license terms and conditions
    +      for use, reproduction, or distribution of Your modifications, or
    +      for any such Derivative Works as a whole, provided Your use,
    +      reproduction, and distribution of the Work otherwise complies with
    +      the conditions stated in this License.
    +
    +   5. Submission of Contributions. Unless You explicitly state otherwise,
    +      any Contribution intentionally submitted for inclusion in the Work
    +      by You to the Licensor shall be under the terms and conditions of
    +      this License, without any additional terms or conditions.
    +      Notwithstanding the above, nothing herein shall supersede or modify
    +      the terms of any separate license agreement you may have executed
    +      with Licensor regarding such Contributions.
    +
    +   6. Trademarks. This License does not grant permission to use the trade
    +      names, trademarks, service marks, or product names of the Licensor,
    +      except as required for reasonable and customary use in describing the
    +      origin of the Work and reproducing the content of the NOTICE file.
    +
    +   7. Disclaimer of Warranty. Unless required by applicable law or
    +      agreed to in writing, Licensor provides the Work (and each
    +      Contributor provides its Contributions) on an "AS IS" BASIS,
    +      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    +      implied, including, without limitation, any warranties or conditions
    +      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    +      PARTICULAR PURPOSE. You are solely responsible for determining the
    +      appropriateness of using or redistributing the Work and assume any
    +      risks associated with Your exercise of permissions under this License.
    +
    +   8. Limitation of Liability. In no event and under no legal theory,
    +      whether in tort (including negligence), contract, or otherwise,
    +      unless required by applicable law (such as deliberate and grossly
    +      negligent acts) or agreed to in writing, shall any Contributor be
    +      liable to You for damages, including any direct, indirect, special,
    +      incidental, or consequential damages of any character arising as a
    +      result of this License or out of the use or inability to use the
    +      Work (including but not limited to damages for loss of goodwill,
    +      work stoppage, computer failure or malfunction, or any and all
    +      other commercial damages or losses), even if such Contributor
    +      has been advised of the possibility of such damages.
    +
    +   9. Accepting Warranty or Additional Liability. While redistributing
    +      the Work or Derivative Works thereof, You may choose to offer,
    +      and charge a fee for, acceptance of support, warranty, indemnity,
    +      or other liability obligations and/or rights consistent with this
    +      License. However, in accepting such obligations, You may act only
    +      on Your own behalf and on Your sole responsibility, not on behalf
    +      of any other Contributor, and only if You agree to indemnify,
    +      defend, and hold each Contributor harmless for any liability
    +      incurred by, or claims asserted against, such Contributor by reason
    +      of your accepting any such warranty or additional liability.
    +
    +   END OF TERMS AND CONDITIONS
    +
    +   APPENDIX: How to apply the Apache License to your work.
    +
    +      To apply the Apache License to your work, attach the following
    +      boilerplate notice, with the fields enclosed by brackets "[]"
    +      replaced with your own identifying information. (Don't include
    +      the brackets!)  The text should be enclosed in the appropriate
    +      comment syntax for the file format. We also recommend that a
    +      file or class name and description of purpose be included on the
    +      same "printed page" as the copyright notice for easier
    +      identification within third-party archives.
    +
    +   Copyright [yyyy] [name of copyright owner]
    +
    +   Licensed under the Apache License, Version 2.0 (the "License");
    +   you may not use this file except in compliance with the License.
    +   You may obtain a copy of the License at
    +
    +       http://www.apache.org/licenses/LICENSE-2.0
    +
    +   Unless required by applicable law or agreed to in writing, software
    +   distributed under the License is distributed on an "AS IS" BASIS,
    +   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +   See the License for the specific language governing permissions and
    +   limitations under the License.
    diff --git a/src/fonts/sources.txt b/src/fonts/sources.txt
    new file mode 100644
    index 00000000..960db369
    --- /dev/null
    +++ b/src/fonts/sources.txt
    @@ -0,0 +1,102 @@
    +# sources.txt -- the fonts DVX converts and ships, pinned (claudeDocs/fonts.md)
    +#
    +# One line per file: id, kind, sha256, url [, name to save it as].
    +# src/fonts/installFonts.sh downloads each file once into toolchains/cache
    +# (refusing one whose sha256 differs) and puts it in toolchains/fonts//:
    +#   file   the file itself (saved as the URL's last part, %-decoded)
    +#   zip    unzipped there
    +#   tar    untarred there (any compression tar knows)
    +#   gz     gunzipped there (its name without .gz)
    +#   b64    base64-decoded there (Android's ?format=TEXT downloads)
    +#   local  a file of this repository (url is its path), copied there
    +# Licences were read at the source for every font (claudeDocs/fonts.md,
    +# "Font list"): OFL without a Reserved Font Name unless noted there,
    +# Apache-2.0, BSD, MIT, Bitstream Vera / DejaVu, X11, WTFPL, CC0, or public
    +# domain.  croscore and Droid Serif state Apache-2.0 only inside the fonts,
    +# so the licence text comes from src/fonts/licenses.
    +
    +dejavu           zip    7576310b219e04159d35ff61dd4a4ec4cdba4f35c00e002a136f00e96a908b0a  https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-fonts-ttf-2.37.zip
    +croscore         tar    672c3487883ec1ef83d9254240d4327b014212abc823d06d15816095867315e1  https://commondatastorage.googleapis.com/chromeos-localmirror/distfiles/croscorefonts-1.31.0.tar.bz2
    +croscore         local  cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30  src/fonts/licenses/Apache-2.0.txt  LICENSE
    +clearsans        file   bb811af889b6a3984944678f04b033a4805c49a66f06cde62ef29902e8f0f06f  https://raw.githubusercontent.com/intel/clear-sans/1993725b24a36af21e7ebaa8977103983b608572/TTF/ClearSans-Regular.ttf
    +clearsans        file   e95b1274e575a12d4f9ed0293c359f92a2a1b73bda862b2432ecc567b330bfeb  https://raw.githubusercontent.com/intel/clear-sans/1993725b24a36af21e7ebaa8977103983b608572/TTF/ClearSans-Bold.ttf
    +clearsans        file   ac2f34c28ea1f755f0660033f50f615c0a30b0f80763b73363cc142113136c43  https://raw.githubusercontent.com/intel/clear-sans/1993725b24a36af21e7ebaa8977103983b608572/TTF/ClearSans-Italic.ttf
    +clearsans        file   89531e842c8b61efad634b6210a725a38fa3f2ac1a736d48a14af89b47361a70  https://raw.githubusercontent.com/intel/clear-sans/1993725b24a36af21e7ebaa8977103983b608572/TTF/ClearSans-BoldItalic.ttf
    +clearsans        file   cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30  https://raw.githubusercontent.com/intel/clear-sans/1993725b24a36af21e7ebaa8977103983b608572/LICENSE.txt
    +gofont           file   197d9f3703b4c00af609178876a8d73e396f64fe438b2c871778566632374be3  https://raw.githubusercontent.com/golang/image/v0.47.0/font/gofont/ttfs/Go-Regular.ttf
    +gofont           file   c18494baa7ea35b8dbfac3861787d360b9c0ab91232562371a09e6fbf09aaa40  https://raw.githubusercontent.com/golang/image/v0.47.0/font/gofont/ttfs/Go-Bold.ttf
    +gofont           file   6efef00d080c8fe68bbe2bf08526294c83d52d7b90b16f938953025c0868cc57  https://raw.githubusercontent.com/golang/image/v0.47.0/font/gofont/ttfs/Go-Italic.ttf
    +gofont           file   d6c33857c7ea7d740a8e1d81d37261782f698e9c17a2c9bf3bb0abbaf99e3c90  https://raw.githubusercontent.com/golang/image/v0.47.0/font/gofont/ttfs/Go-Bold-Italic.ttf
    +gofont           file   8bc66a0154bbf69cd24e5bde41a12ec9495c8a242c5def2255e4a164900f1ed7  https://raw.githubusercontent.com/golang/image/v0.47.0/font/gofont/ttfs/Go-Mono.ttf
    +gofont           file   b165f1a142dc88d3c388de2c89d3ab62dd04620ef21ad07f43fb51f77857edc3  https://raw.githubusercontent.com/golang/image/v0.47.0/font/gofont/ttfs/Go-Mono-Bold.ttf
    +gofont           file   10d5f73519ea32b4882897a9bec6c42ba2ca27d6c06ba9a0e55ae38a383cf7e6  https://raw.githubusercontent.com/golang/image/v0.47.0/font/gofont/ttfs/Go-Mono-Italic.ttf
    +gofont           file   cb43aca5ab36631c0b4679831017cf249c33db4f721daad7379e04bba383ef1f  https://raw.githubusercontent.com/golang/image/v0.47.0/font/gofont/ttfs/Go-Mono-Bold-Italic.ttf
    +gofont           file   dc588a7ba9130043b67c2380cc03cc6644ed3557f6d5e8589b96acba1c64e343  https://raw.githubusercontent.com/golang/image/v0.47.0/font/gofont/ttfs/README
    +droidserif       b64    a02762ab1e5a1b6484a5e857198972e076daf786023c003f7e889d61b2085aeb  https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-4.1.1_r1/data/fonts/DroidSerif-Regular.ttf?format=TEXT  DroidSerif-Regular.ttf
    +droidserif       b64    c1acef8f1d6874833aa800624ea7a6a51fe17722a539779f2ff3044c563daadf  https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-4.1.1_r1/data/fonts/DroidSerif-Bold.ttf?format=TEXT  DroidSerif-Bold.ttf
    +droidserif       b64    c41a69084b5f7b05ce7a4cd0c180acd12237e3635a5d01f4e143ec816f0efcf0  https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-4.1.1_r1/data/fonts/DroidSerif-Italic.ttf?format=TEXT  DroidSerif-Italic.ttf
    +droidserif       b64    b687dd70b7a783d068ff36b8ad98ee461b29410c6ec574f7cdbfc5bbd629b666  https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-4.1.1_r1/data/fonts/DroidSerif-BoldItalic.ttf?format=TEXT  DroidSerif-BoldItalic.ttf
    +droidserif       local  cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30  src/fonts/licenses/Apache-2.0.txt  LICENSE
    +charter          tar    de2f238b4cd72db4228a0ba67829d76a2b7c039e22993d66a722ee385248c628  https://www.x.org/releases/individual/font/font-bitstream-type1-1.0.4.tar.xz
    +notoserif        file   19e72cd8d595fae5bd74a5206f5d938512e1183d4fed7abb1ec1be1d7efa5f88  https://raw.githubusercontent.com/notofonts/notofonts.github.io/4e15001b9fb484cc871d7b2ad930cd2226edf260/fonts/NotoSerif/hinted/ttf/NotoSerif-Regular.ttf
    +notoserif        file   96656aa5cec8f1d6fd0e804c1fad397e1a1cfa082e6642124e0bda68cd8363ce  https://raw.githubusercontent.com/notofonts/notofonts.github.io/4e15001b9fb484cc871d7b2ad930cd2226edf260/fonts/NotoSerif/hinted/ttf/NotoSerif-Bold.ttf
    +notoserif        file   749e80e313ef711f9373c6cce17c72297ef05490b3dcda7967d1d5d90bf1183f  https://raw.githubusercontent.com/notofonts/notofonts.github.io/4e15001b9fb484cc871d7b2ad930cd2226edf260/fonts/NotoSerif/hinted/ttf/NotoSerif-Italic.ttf
    +notoserif        file   c710c5b9cf354ae46e7a10472a08019b28220fafeb5887a482a76856f8f6fc0b  https://raw.githubusercontent.com/notofonts/notofonts.github.io/4e15001b9fb484cc871d7b2ad930cd2226edf260/fonts/NotoSerif/hinted/ttf/NotoSerif-BoldItalic.ttf
    +notoserif        file   cee9892f9f0cc8fe882c9e9537ee6a89621d86ee7ceaf70b02e2b2b1c25c061a  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/notoserif/OFL.txt
    +robotoslab       file   fd4d98f8403041d58d67735f4549a42ac7cfc79464d4fac5f02345e1404dd2ca  https://raw.githubusercontent.com/googlefonts/robotoslab/67af3ce9c4ca574419e1295b6165a2eeee112e6e/fonts/ttf/RobotoSlab-Regular.ttf
    +robotoslab       file   09cd9e359296e2f9482e2445ba921041913b55e652645df083079a85a272fce4  https://raw.githubusercontent.com/googlefonts/robotoslab/67af3ce9c4ca574419e1295b6165a2eeee112e6e/fonts/ttf/RobotoSlab-Bold.ttf
    +robotoslab       file   cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/apache/robotoslab/LICENSE.txt
    +atkinson         file   7fb917c89019896d0b52ee84b7cbb3304c18cb90b19a62f5e32712bd23e97669  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/atkinsonhyperlegible/AtkinsonHyperlegible-Regular.ttf
    +atkinson         file   5a3b0c8cc8ca545155150b4512a1fa248298df121c50d6557e651e61fbdab92f  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/atkinsonhyperlegible/AtkinsonHyperlegible-Bold.ttf
    +atkinson         file   021beda4d3c6edfc78872e436d74009f9a1bcb294331908fe5747c61d3dcc514  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/atkinsonhyperlegible/AtkinsonHyperlegible-Italic.ttf
    +atkinson         file   7a46f1b0ef2f64b61065c1a6b5a11fcb9b019b44062d88ba121a083cc39e86bf  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/atkinsonhyperlegible/AtkinsonHyperlegible-BoldItalic.ttf
    +atkinson         file   f32d22b3908fcad2c86a74000614ec22e6a7f66ea7e867e616026a27aebdc143  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/atkinsonhyperlegible/OFL.txt
    +firacode         zip    0949915ba8eb24d89fd93d10a7ff623f42830d7c5ffc3ecbf960e4ecad3e3e79  https://github.com/tonsky/FiraCode/releases/download/6.2/Fira_Code_v6.2.zip
    +firacode         file   1d41e10031ab125302780a05ec4c91d218e47db0c7e37cf315cce5e608cdc25c  https://raw.githubusercontent.com/tonsky/FiraCode/6.2/LICENSE
    +jetbrainsmono    zip    6f6376c6ed2960ea8a963cd7387ec9d76e3f629125bc33d1fdcd7eb7012f7bbf  https://github.com/JetBrains/JetBrainsMono/releases/download/v2.304/JetBrainsMono-2.304.zip
    +comicneue        zip    746036c97f2e73012734ef9630c0f0253abfe45f038f16119e74497270945740  https://github.com/crozynski/comicneue/releases/download/2.51/comicneue-master.zip
    +comicrelief      file   9ef4958ab06385d91f635f83a15569786b243f9d010cfb3f9a5cdda593c7bc22  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/comicrelief/ComicRelief-Regular.ttf
    +comicrelief      file   33aee007212b696afd57d50c901135cd723cd188f7ad98f3a1c572ea64e8f0d5  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/comicrelief/ComicRelief-Bold.ttf
    +comicrelief      file   009b3056cef5c9bcaff872bfb0e27fa4a0f8cc6f6984cc490caf5618748d766b  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/comicrelief/OFL.txt
    +patrickhand      file   0f173b3e6cb6d1af25babf7f0057c5ac4ee11f9992b0469bb817e967ef4ad0fc  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/patrickhand/PatrickHand-Regular.ttf
    +patrickhand      file   377f4f9c19e935228552478eb68cc2ed82910988a60ba60e2ac73b09f32d02d1  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/patrickhand/OFL.txt
    +pangolin         file   cf4f0a46704c7859d4943320fce6c6d06cc7dc450ef971c3676ef36f9f482299  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/pangolin/Pangolin-Regular.ttf
    +pangolin         file   b4fc1d55ce610f255c5712f8169543f9610536d002deb99bbc99bfbba9f8a560  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/pangolin/OFL.txt
    +pacifico         file   5b6c0d5334a7bf77dea52b975c5a0c408878c0f7115ed5b6fb151f634b7bf701  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/pacifico/Pacifico-Regular.ttf
    +pacifico         file   a47e5daeda73568969395c656823102678f2eefb0d7d7ecb47aac4cc17e42204  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/pacifico/OFL.txt
    +anton            file   a4ba3a92350ebb031da0cb47630ac49eb265082ca1bc0450442f4a83ab947cab  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/anton/Anton-Regular.ttf
    +anton            file   ee67e6ee22790b7929f1a3769ca2801d565c64b5a9096942c1adf5596de9c9e4  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/anton/OFL.txt
    +bangers          file   4160a7311de9342674cce9160cde9fcbb30f48190397d86ff1b70b455af65824  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/bangers/Bangers-Regular.ttf
    +bangers          file   630dd5a307c0657b094d324e069b390edf90182fe5003639e06c02a0e5769af7  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/bangers/OFL.txt
    +exo2             file   205a448676a2586f9c57c25f3d5c58ca8db7e6cf5edf7506783a010c6fe2bfb5  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/exo2/Exo2%5Bwght%5D.ttf
    +exo2             file   bad568cea9ed23d6ae4aea41ad3f7747752952aabdd85d7f01474af392a21b32  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/exo2/Exo2-Italic%5Bwght%5D.ttf
    +exo2             file   7f95a8eeecc6904ffc81cd10cc8ebc7fcee72a67922924bf45f02dff03b88127  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/exo2/OFL.txt
    +specialelite     file   a776fcb4ceb8bdf03e2967688ebdad42680de5b91a7e62c17e718ae212d14bc4  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/apache/specialelite/SpecialElite-Regular.ttf
    +specialelite     file   cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/apache/specialelite/LICENSE.txt
    +sancreek         file   8f87c42f6e6ba97125cd975c3d7d48902fc227d805948e678d5293a300e3fe55  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/sancreek/Sancreek-Regular.ttf
    +sancreek         file   69f01b79c1be37a734736a94a12c55cb28d09ee93e4febc9db109cec608f9ae7  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/sancreek/OFL.txt
    +permanentmarker  file   28f82c8a7943cb8e9d599f8554da1d4fc75dbcf69b9885ad6c0611d20c6946c5  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/apache/permanentmarker/PermanentMarker-Regular.ttf
    +permanentmarker  file   cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/apache/permanentmarker/LICENSE.txt
    +miscfixed        tar    79abe361f58bb21ade9f565898e486300ce1cc621d5285bec26e14b6a8618fed  https://www.x.org/releases/individual/font/font-misc-misc-1.1.3.tar.xz
    +spleen           tar    ec42925c6b56d2138c862b2f97147c872e472f674bf03423417d827a08d69a89  https://github.com/fcambus/spleen/releases/download/2.2.0/spleen-2.2.0.tar.gz
    +gohufont         tar    758d62c9350d51ae3738aff4bbcefa9ea6d173baf5b169232c895b6de3a1ba81  https://font.gohu.org/gohufont-2.1.tar.gz
    +cozette          file   8d740166af3a14053773ac7a8846bf288c8818a44f73ab28d4725d91b03a5639  https://github.com/the-moonwitch/Cozette/releases/download/v.1.30.0/cozette.bdf
    +cozette          file   5d7f0dad37ceef2dc1e9c23e85fbf18629c4eaffe9a919fa4f5b618b46766d23  https://raw.githubusercontent.com/the-moonwitch/Cozette/v.1.30.0/LICENSE
    +unifont          gz     2989e0211030d8219fd931aba599fc95c86bc327ce1423848ef949783b208257  https://ftp.gnu.org/gnu/unifont/unifont-18.0.01/unifont-18.0.01.bdf.gz
    +unifont          file   869692af094c57fb7258c57fe26820c759319603321d0ffeb278de3651763ded  https://unifoundry.com/OFL-1.1.txt
    +arkpixel         zip    b122a98a304da1ad1c91a303f7bcad92262f6f391b89fa8aeea804d654070314  https://github.com/TakWolf/ark-pixel-font/releases/download/2026.09.25/ark-pixel-font-10px-proportional-bdf-v2026.09.25.zip
    +arkpixel         zip    d34b2c84db223a21b4be08943fed156accb311177aaed04c403459ae4a58ebef  https://github.com/TakWolf/ark-pixel-font/releases/download/2026.09.25/ark-pixel-font-12px-proportional-bdf-v2026.09.25.zip
    +pixeloperator    zip    e2b4e2d1344a44b85031ab1b151afeeb74bf6c3db9ca2d762b00a10a48ed9730  https://fontlibrary.org/assets/downloads/pixel-operator/c2b01134add5f65e795cf4ba2ec4719b/pixel-operator.zip
    +x11adobe         tar    1281a62dbeded169e495cae1a5b487e1f336f2b4d971d92911c59c103999b911  https://www.x.org/releases/individual/font/font-adobe-75dpi-1.0.4.tar.xz
    +x11adobe         tar    b67aff445e056328d53f9732d39884f55dd8d303fc25af3dbba33a8ba35a9ccf  https://www.x.org/releases/individual/font/font-adobe-100dpi-1.0.4.tar.xz
    +x11bitstream     tar    aaeb34d87424a9c2b0cf0e8590704c90cb5b42c6a3b6a0ef9e4676ef773bf826  https://www.x.org/releases/individual/font/font-bitstream-75dpi-1.0.4.tar.xz
    +silkscreen       file   c845473330b94c2079ce9af01c51ac8ba2d99c24f4d14c039843bbb8e642ebd8  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/silkscreen/Silkscreen-Regular.ttf
    +silkscreen       file   768476aa712d4f5c3e18d3bce80f980a8bd3f72b7094d22ec5e768df3acfed61  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/silkscreen/Silkscreen-Bold.ttf
    +silkscreen       file   86c5e9c9382cdcc5948704fdfe60f2aa164a719746931219a42736ecd9cefbd3  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/silkscreen/OFL.txt
    +tiny5            file   588626f2c1b164f55927c0d8123f4bc134651aad7d257da68fc19b0824b0d8fa  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/tiny5/Tiny5%5BBLED%2CJITT%2CROND%2Cwdth%2Cwght%5D.ttf
    +tiny5            file   690fda321d0ebce403249c8d61dadddcee09dc8813424cc5a8f982efcff9a9a3  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/tiny5/OFL.txt
    +departuremono    zip    bf3e48059aeef4617ec585bdea81dcc3491c576b3e7a472f52faf40e09ee5c3a  https://github.com/rektdeckard/departure-mono/releases/download/v1.500/DepartureMono-1.500.zip
    +kirsch           file   6ded338bf6cc87e66dbc3dec7382d2054d8c2a11a409f1270649f7a28fe0e36a  https://github.com/molarmanful/kirsch/releases/download/v0.7.3/kirsch.bdf
    +kirsch           file   d889040dd7429249b02de25c873d4554c119a13ea4ebc91962efa48b9e90f6c7  https://raw.githubusercontent.com/molarmanful/kirsch/v0.7.3/LICENSE
    +bungee           file   c4f5361ce120af3e6b9156d0bf379fa19cda2ea0cd18ac01fd99596c6bf66e3f  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/bungee/Bungee-Regular.ttf
    +bungee           file   d5787a50dde5be6c6daecab9ed459939b1bc37cff0d0e00257eaf1ec2cf4c16c  https://raw.githubusercontent.com/google/fonts/66928797f271fed5ee428544e7229685e2d90127/ofl/bungee/OFL.txt
    diff --git a/src/include/basic/comm.bas b/src/include/basic/comm.bas
    index 7529c2f5..5e5333f5 100644
    --- a/src/include/basic/comm.bas
    +++ b/src/include/basic/comm.bas
    @@ -34,12 +34,12 @@
     '      speak the same protocol.
     '
     '   3. Encrypted serial (CommHandshake + encrypt flag) -- adds a key
    -'      exchange, encryption and tamper detection on top of packets. Use
    +'      exchange, encryption, and tamper detection on top of packets. Use
     '      CommSend with encrypt=True after CommHandshake. Pass the same
     '      passphrase to CommHandshake on both ends to also authenticate
     '      the peer; without one the link is private but unauthenticated.
     '
    -' SerWrite, SerRead$, CommSend and CommRecv$ carry any bytes, CHR$(0) too.
    +' SerWrite, SerRead$, CommSend, and CommRecv$ carry any bytes, CHR$(0) too.
     '
     ' Usage: Add this file to your project.
     
    @@ -128,7 +128,7 @@ DECLARE LIBRARY "basrt"
         ' why not.
         DECLARE FUNCTION SerOpen(BYVAL com AS LONG, BYVAL baud AS LONG, BYVAL dataBits AS LONG, BYVAL parity AS STRING, BYVAL stopBits AS LONG, BYVAL handshake AS LONG) AS LONG
     
    -    ' Change the speed, data bits, parity, stop bits and handshake of an
    +    ' Change the speed, data bits, parity, stop bits, and handshake of an
         ' open port (the same arguments as SerOpen). Returns True on success.
         DECLARE FUNCTION SerSet(BYVAL com AS LONG, BYVAL baud AS LONG, BYVAL dataBits AS LONG, BYVAL parity AS STRING, BYVAL stopBits AS LONG, BYVAL handshake AS LONG) AS LONG
     
    @@ -137,7 +137,7 @@ DECLARE LIBRARY "basrt"
         DECLARE FUNCTION SerLastError(BYVAL com AS LONG) AS LONG
     
         ' The settings of an open port: speed, data bits, parity ("N", "E",
    -    ' "O", "M" or "S"), stop bits and handshake (SER_HANDSHAKE_). When the
    +    ' "O", "M", or "S"), stop bits, and handshake (SER_HANDSHAKE_). When the
         ' port is not open the numbers are an SER_ERR_ code and the parity "".
         DECLARE FUNCTION SerGetBaud(BYVAL com AS LONG) AS LONG
         DECLARE FUNCTION SerGetDataBits(BYVAL com AS LONG) AS LONG
    @@ -187,7 +187,7 @@ DECLARE LIBRARY "basrt"
         DECLARE SUB SerSetRts(BYVAL com AS LONG, BYVAL state AS LONG)
     
         ' True when the line is up. DTR and RTS are ours; CTS (clear to
    -    ' send), DSR (data set ready) and DCD (carrier detect) come from the
    +    ' send), DSR (data set ready), and DCD (carrier detect) come from the
         ' other end.
         DECLARE FUNCTION SerGetDtr(BYVAL com AS LONG) AS LONG
         DECLARE FUNCTION SerGetRts(BYVAL com AS LONG) AS LONG
    @@ -232,7 +232,7 @@ DECLARE LIBRARY "basrt"
         DECLARE FUNCTION CommPending(BYVAL handle AS LONG) AS LONG
     
         ' The COMM_ result of the last CommHandshake, CommHandshakeStep,
    -    ' CommSend, CommSetPlain or CommPoll on the link: COMM_OK when it
    +    ' CommSend, CommSetPlain, or CommPoll on the link: COMM_OK when it
         ' worked, COMM_ERR_AUTH for a passphrase mismatch, ...
         DECLARE FUNCTION CommLastError(BYVAL handle AS LONG) AS LONG
     
    diff --git a/src/include/basic/commdlg.bas b/src/include/basic/commdlg.bas
    index dc798417..c474ff73 100644
    --- a/src/include/basic/commdlg.bas
    +++ b/src/include/basic/commdlg.bas
    @@ -22,8 +22,8 @@
     
     ' commdlg.bas -- Common Dialog Library for DVX BASIC
     '
    -' Provides file dialogs, input dialogs, the color dialog, and other
    -' common UI dialogs.
    +' Provides file dialogs, input dialogs, the color and font dialogs, and
    +' other common UI dialogs.
     ' Wrappers handle the AppContextT parameter internally so BASIC
     ' programs just pass the visible arguments.
     '
    @@ -52,7 +52,7 @@ DECLARE LIBRARY "basrt"
         ' Show a modal text input box. Returns entered text, or "" if cancelled.
         DECLARE FUNCTION DlgInputBox$(BYVAL title AS STRING, BYVAL prompt AS STRING, BYVAL defaultText AS STRING) AS STRING
     
    -    ' Returns True if the most recent DlgInputBox$, DlgIntInput or
    +    ' Returns True if the most recent DlgInputBox$, DlgIntInput, or
         ' DlgIntInputStep call was cancelled, False if the user clicked OK
         ' (even with an empty field or the default number).
         DECLARE FUNCTION DlgInputCancelled() AS INTEGER
    @@ -60,7 +60,7 @@ DECLARE LIBRARY "basrt"
         ' Show a message box with your own buttons.  buttons$ is
         ' pipe-delimited and & marks each button's access key, e.g.
         ' "&Yes|&No|Yes to &All|&Cancel" (1 to 6 buttons).  icon is vbQuestion,
    -    ' vbInformation, vbExclamation, vbCritical or 0.  Returns the chosen
    +    ' vbInformation, vbExclamation, vbCritical, or 0.  Returns the chosen
         ' button's index (0 = the first, which Enter presses; Esc and closing
         ' the box choose the last), or -1 if buttons$ is empty or too long.
         DECLARE FUNCTION DlgButtons(BYVAL title AS STRING, BYVAL message AS STRING, BYVAL buttons AS STRING, BYVAL icon AS LONG) AS INTEGER
    @@ -77,12 +77,26 @@ DECLARE LIBRARY "basrt"
         ' DlgIntInput with the spinner arrows moving by stepVal.
         DECLARE FUNCTION DlgIntInputStep(BYVAL title AS STRING, BYVAL prompt AS STRING, BYVAL defaultVal AS LONG, BYVAL minVal AS LONG, BYVAL maxVal AS LONG, BYVAL stepVal AS LONG) AS LONG
     
    -    ' Show the color dialog: basic colors, Red/Green/Blue sliders and a
    +    ' Show the color dialog: basic colors, Red/Green/Blue sliders, and a
         ' preview, starting at defaultColor.  Returns the chosen color as
         ' RGB() makes it (ForeColor/BackColor use the same form), or -1 if
         ' cancelled.
         DECLARE FUNCTION DlgColor(BYVAL title AS STRING, BYVAL defaultColor AS LONG) AS LONG
     
    +    ' Show the font dialog: the font families, their styles and sizes,
    +    ' Underline and Strikeout, and a sample, starting at family$ in size
    +    ' (pixels) and style.  style adds up the FONT_BOLD, FONT_ITALIC,
    +    ' FONT_UNDERLINE, and FONT_STRIKE constants of font.bas (add it to the
    +    ' project too, or use 1, 2, 4, and 8).  flags DLG_FONT_FIXED offers
    +    ' only fixed-pitch families.  Returns True when the user chose OK;
    +    ' then DlgFontName$, DlgFontSize, and DlgFontStyle give the font
    +    ' chosen (after Cancel, the font the dialog started at), ready for a
    +    ' control's or form's FontName, FontSize, FontBold ...
    +    DECLARE FUNCTION DlgFont(BYVAL title AS STRING, BYVAL family AS STRING, BYVAL size AS LONG, BYVAL style AS LONG, BYVAL flags AS LONG) AS LONG
    +    DECLARE FUNCTION DlgFontName$() AS STRING
    +    DECLARE FUNCTION DlgFontSize() AS LONG
    +    DECLARE FUNCTION DlgFontStyle() AS LONG
    +
         ' Show a "Save changes?" prompt with Yes/No/Cancel buttons.
         DECLARE FUNCTION DlgPromptSave(BYVAL title AS STRING) AS INTEGER
     END DECLARE
    @@ -91,3 +105,6 @@ END DECLARE
     CONST DVX_SAVE_YES    = 1
     CONST DVX_SAVE_NO     = 2
     CONST DVX_SAVE_CANCEL = 3
    +
    +' DlgFont flags: only fixed-pitch families (for a text editor or a terminal)
    +CONST DLG_FONT_FIXED  = 1
    diff --git a/src/include/basic/compiler.bas b/src/include/basic/compiler.bas
    index 8fcd0ddc..9da46d9f 100644
    --- a/src/include/basic/compiler.bas
    +++ b/src/include/basic/compiler.bas
    @@ -42,11 +42,11 @@
     ' Lists are rows ready for ListView.SetRows: one row per line, the cells
     ' separated by TAB characters.
     '
    -' Source sets, modules and projects are numbers (0 means the call
    +' Source sets, modules, and projects are numbers (0 means the call
     ' failed); free each when done.  Whatever a program leaves open is freed
     ' when it ends.  A project (ProjOpen / ProjNew) is the project file
     ' (.dbp) as the DVX BASIC IDE edits it: its files and its settings.
    -' ProjCtlNew$, ProjCtlHeader$, ProjCtlSetHeader$ and ProjCtlKeys$ make a
    +' ProjCtlNew$, ProjCtlHeader$, ProjCtlSetHeader$, and ProjCtlKeys$ make a
     ' UserControl (.ctl) and read and change its header the way the IDE's
     ' File > New UserControl and Properties window do.
     '
    @@ -81,12 +81,12 @@ DECLARE LIBRARY "basdev"
         DECLARE FUNCTION CompSourceAdd(BYVAL src AS LONG, BYVAL path AS STRING, BYVAL text AS STRING, BYVAL kind AS LONG) AS LONG
     
         ' Replace the set with the files of the project dbpPath, read from
    -    ' disk. Its Option Explicit, startup form, name and help file come
    +    ' disk. Its Option Explicit, startup form, name, and help file come
         ' along. True when every file could be read.
         DECLARE FUNCTION CompSourceFromProject(BYVAL src AS LONG, BYVAL dbpPath AS STRING) AS LONG
     
         ' The files of the set in compile order, one row each: the path as
    -    ' the line mapping below names it, the file line its code starts on
    +    ' the line mapping below names it, the file line its code starts on,
         ' and the line of the concatenated source that line is.
         DECLARE FUNCTION CompSourceFiles$(BYVAL src AS LONG) AS STRING
     
    @@ -136,8 +136,8 @@ DECLARE LIBRARY "basdev"
         DECLARE SUB CompModuleFree(BYVAL module AS LONG)
     
         ' Build the application outPath (.app) from a module and its source
    -    ' set: a project's name, icon, help file and the rest of its
    -    ' [Project] settings, its forms and its UserControls.  A control
    +    ' set: a project's name, icon, help file, and the rest of its
    +    ' [Project] settings, its forms, and its UserControls.  A control
         ' project's set (Type = Control) builds a control file instead
         ' (outPath NAME.wgt, the IDE's Make Control): its UserControls with
         ' their toolbox pictures and names, for every program once it is
    @@ -177,7 +177,7 @@ DECLARE LIBRARY "basdev"
         ' on code that does not compile.
         DECLARE FUNCTION CompProcList$(BYVAL source AS STRING) AS STRING
     
    -    ' True when ident can name a variable, procedure or control.
    +    ' True when ident can name a variable, procedure, or control.
         DECLARE FUNCTION CompIsIdent(BYVAL ident AS STRING) AS LONG
     
         ' The language's keywords: CompKeyword$(0) to
    @@ -193,9 +193,9 @@ DECLARE LIBRARY "basdev"
         DECLARE FUNCTION CompKeywordReserved(BYVAL index AS LONG) AS LONG
     
         ' The tokens of source text as the compiler reads them, one row each:
    -    ' line, column, length, kind and the text.  The kinds are Keyword,
    +    ' line, column, length, kind, and the text.  The kinds are Keyword,
         ' Identifier, String (quotes included), Number, Date, Symbol, Comment
    -    ' (' or REM to the end of the line) and Error (text the compiler
    +    ' (' or REM to the end of the line), and Error (text the compiler
         ' cannot read, such as a string without its closing quote).  Line
         ' ends are not listed.
         DECLARE FUNCTION CompTokens$(BYVAL source AS STRING) AS STRING
    @@ -250,7 +250,7 @@ DECLARE LIBRARY "basdev"
     
         ' Its files, numbered from 0: the name as the project lists it
         ' (relative to the project's folder), the full path, and the kind
    -    ' (COMP_KIND_MODULE, COMP_KIND_FORM, COMP_KIND_CLASS or
    +    ' (COMP_KIND_MODULE, COMP_KIND_FORM, COMP_KIND_CLASS, or
         ' COMP_KIND_CONTROL).
         DECLARE FUNCTION ProjFileCount(BYVAL project AS LONG) AS LONG
         DECLARE FUNCTION ProjFile$(BYVAL project AS LONG, BYVAL index AS LONG) AS STRING
    @@ -261,13 +261,24 @@ DECLARE LIBRARY "basdev"
         DECLARE FUNCTION ProjFileFind(BYVAL project AS LONG, BYVAL fileName AS STRING) AS LONG
     
         ' Add a file of the project's folder (its kind from its extension: .bas,
    -    ' .cls, .ctl or .frm).  Its index, or -1 when it is already in the
    +    ' .cls, .ctl, or .frm).  Its index, or -1 when it is already in the
         ' project.
         DECLARE FUNCTION ProjAddFile(BYVAL project AS LONG, BYVAL fileName AS STRING) AS LONG
     
         ' Take a file out of the project (the file itself stays).
         DECLARE FUNCTION ProjRemoveFile(BYVAL project AS LONG, BYVAL index AS LONG) AS LONG
     
    +    ' The .DFN fonts the built program carries (as font resources, its own
    +    ' while it runs: where a family is installed too, the sizes and styles
    +    ' it has come first). ProjFont$ gives one's path as the project lists
    +    ' it (relative to its folder); ProjAddFont adds one (its index, or -1
    +    ' when it is listed already) and ProjRemoveFont takes one out (the
    +    ' file stays).
    +    DECLARE FUNCTION ProjFontCount(BYVAL project AS LONG) AS LONG
    +    DECLARE FUNCTION ProjFont$(BYVAL project AS LONG, BYVAL index AS LONG) AS STRING
    +    DECLARE FUNCTION ProjAddFont(BYVAL project AS LONG, BYVAL fileName AS STRING) AS LONG
    +    DECLARE FUNCTION ProjRemoveFont(BYVAL project AS LONG, BYVAL index AS LONG) AS LONG
    +
         ' Rename a file on disk and in the project together; the project file
         ' is saved at once.  False with CompError$ saying why (an existing
         ' file is never overwritten).
    @@ -277,7 +288,7 @@ DECLARE LIBRARY "basdev"
         ' for a control project, which builds a control file; empty for a
         ' program), Author, Publisher, Version, Copyright, Description, Icon,
         ' HelpFile, CommandArgs (the IDE's COMMAND$ for runs), StartupForm,
    -    ' OptionExplicit and MultiInstance (these two read "true" or "false";
    +    ' OptionExplicit, and MultiInstance (these two read "true" or "false";
         ' set them with "true" / "false", True or False).  ProjSet returns
         ' False for a name it does not know.
         DECLARE FUNCTION ProjGet$(BYVAL project AS LONG, BYVAL setting AS STRING) AS STRING
    @@ -294,12 +305,12 @@ DECLARE LIBRARY "basdev"
     
         ' The header keys, one row each in the order the IDE's Properties
         ' window lists them: the key and the kind of value it takes -- Name
    -    ' (the control's name), Integer, Boolean, Color (&HBBGGRR&) or Text.
    +    ' (the control's name), Integer, Boolean, Color (&HBBGGRR&), or Text.
         DECLARE FUNCTION ProjCtlKeys$() AS STRING
     
         ' The text of a new UserControl called name, as the IDE's File > New
         ' UserControl makes it: its header with Width and Height, Option
    -    ' Explicit and an empty UserControl_Paint.  Empty, with CompError$
    +    ' Explicit, and an empty UserControl_Paint.  Empty, with CompError$
         ' saying why, when the name is not a valid name.
         DECLARE FUNCTION ProjCtlNew$(BYVAL controlName AS STRING) AS STRING
     
    diff --git a/src/include/basic/conn.bas b/src/include/basic/conn.bas
    index 3f83822c..e94254b6 100644
    --- a/src/include/basic/conn.bas
    +++ b/src/include/basic/conn.bas
    @@ -23,7 +23,7 @@
     ' conn.bas -- Connections Library for DVX BASIC
     '
     ' One way to talk to another computer, whatever is in between: a serial
    -' port, a modem (it dials, redials when the line is busy and notices when
    +' port, a modem (it dials, redials when the line is busy, and notices when
     ' the carrier is lost), the secure link to the DVX host proxy (encrypted;
     ' authenticated too with a passphrase), or Telnet over the network. A
     ' connection is a handle (0 means the call failed):
    @@ -40,14 +40,14 @@
     ' With ConnAttach a Terminal control shows what arrives (ConnPoll puts it
     ' there) and what is typed in it is sent. ConnPoll also answers ENQ with
     ' the answerback (ConnSetAnswerback), appends what arrives to a capture
    -' file (ConnCapture) and notices a ZMODEM sender starting a download
    +' file (ConnCapture), and notices a ZMODEM sender starting a download
     ' (ConnZmodemRequest). Without a Terminal, read with ConnRead$.
     '
    -' ConnSendFiles and ConnReceiveFiles start XMODEM, YMODEM or ZMODEM
    +' ConnSendFiles and ConnReceiveFiles start XMODEM, YMODEM, or ZMODEM
     ' transfers on the connection (XFER_ constants and functions: xfer.bas).
     ' They return transfer handles: follow them with XferStatus, XferBytes,
     ' ... and free them with XferClose. While a transfer runs, ConnPoll moves
    -' it along, the Terminal shows nothing and typing is not sent; a
    +' it along, the Terminal shows nothing, and typing is not sent; a
     ' connection that drops cancels it.
     '
     ' ConnWrite and ConnRead$ carry any bytes, CHR$(0) too. Every connection
    @@ -80,7 +80,7 @@ CONST CONN_FLOW_RTSCTS  = 2
     DECLARE LIBRARY "conn"
     
         ' =========================================================
    -    ' Creating, setting up and opening
    +    ' Creating, setting up, and opening
         ' =========================================================
     
         ' A closed connection with the defaults: COM1 at 9600 bps, 8 data
    @@ -90,13 +90,13 @@ DECLARE LIBRARY "conn"
         DECLARE FUNCTION ConnCreate() AS LONG
     
         ' The COM port (1-4), speed, data bits (5-8), parity ("N", "E", "O",
    -    ' "M" or "S"), stop bits (1 or 2) and flow control (CONN_FLOW_) for
    -    ' serial ports, modems and the secure link.
    +    ' "M", or "S"), stop bits (1 or 2), and flow control (CONN_FLOW_) for
    +    ' serial ports, modems, and the secure link.
         DECLARE SUB ConnSetSerial(BYVAL handle AS LONG, BYVAL com AS LONG, BYVAL bps AS LONG, BYVAL dataBits AS LONG, BYVAL parity AS STRING, BYVAL stopBits AS LONG, BYVAL flow AS LONG)
     
         ' Modems: the setup command sent first ("" for none), the dial
         ' command, the number ("" = no dialing: type AT commands yourself),
    -    ' how many times BUSY or NO ANSWER is dialed again and the seconds
    +    ' how many times BUSY or NO ANSWER is dialed again, and the seconds
         ' to wait before each.
         DECLARE SUB ConnSetModem(BYVAL handle AS LONG, BYVAL initString AS STRING, BYVAL dialPrefix AS STRING, BYVAL phone AS STRING, BYVAL redialCount AS LONG, BYVAL redialDelay AS LONG)
     
    @@ -133,7 +133,7 @@ DECLARE LIBRARY "conn"
         ' Call it often (a Timer with a short Interval).
         DECLARE SUB ConnPoll(BYVAL handle AS LONG)
     
    -    ' CONN_CLOSED, CONN_OPENING (dialing, setting up) or CONN_OPEN.
    +    ' CONN_CLOSED, CONN_OPENING (dialing, setting up), or CONN_OPEN.
         DECLARE FUNCTION ConnState(BYVAL handle AS LONG) AS LONG
     
         ' What is happening ("Dialing 555-1234", "Connected to COM1 at 9600
    diff --git a/src/include/basic/debug.bas b/src/include/basic/debug.bas
    index 65891948..bef7dbfa 100644
    --- a/src/include/basic/debug.bas
    +++ b/src/include/basic/debug.bas
    @@ -39,7 +39,7 @@
     ' RUN_PAUSED. When it stops inside one of its event handlers -- or in
     ' its forms' Load code, which runs inside RunStart -- it waits right
     ' there while your program's Timer and menus keep working, so they can
    -' step, continue or stop it; RunPoll called meanwhile returns at once
    +' step, continue, or stop it; RunPoll called meanwhile returns at once
     ' (and RunStart returns only after the pause). The program's form
     ' captions show "[break]" while it is paused.
     '
    @@ -69,7 +69,7 @@ DECLARE LIBRARY "basdev"
         DECLARE FUNCTION RunCreate(BYVAL module AS LONG, BYVAL src AS LONG, BYVAL appDir AS STRING, BYVAL args AS STRING) AS LONG
     
         ' Load the program's forms, show the startup form ("" = the
    -    ' project's, else the first) and let RunPoll run its code.
    +    ' project's, else the first), and let RunPoll run its code.
         ' breakOnStart pauses at its first line. True when started.
         DECLARE FUNCTION RunStart(BYVAL run AS LONG, BYVAL startupForm AS STRING, BYVAL breakOnStart AS LONG) AS LONG
     
    @@ -118,7 +118,7 @@ DECLARE LIBRARY "basdev"
         DECLARE FUNCTION DbgLine(BYVAL run AS LONG) AS LONG
         DECLARE FUNCTION DbgProc$(BYVAL run AS LONG) AS STRING
     
    -    ' The variables visible where it is paused, one row each: name, type
    +    ' The variables visible where it is paused, one row each: name, type,
         ' and value.
         DECLARE FUNCTION DbgLocals$(BYVAL run AS LONG) AS STRING
     
    @@ -132,7 +132,7 @@ DECLARE LIBRARY "basdev"
         DECLARE FUNCTION DbgEval$(BYVAL run AS LONG, BYVAL expr AS STRING) AS STRING
     
         ' A line typed into an Immediate window: "x = expr" changes the
    -    ' paused program's variable; a line starting with PRINT, DIM or LET
    +    ' paused program's variable; a line starting with PRINT, DIM, or LET
         ' runs as it is; anything else is printed. Returns what it showed.
         ' Without a paused program (run 0) only expressions and statements
         ' that need none of its variables work.
    diff --git a/src/include/basic/designer.bas b/src/include/basic/designer.bas
    index bad7af08..11b0c9c9 100644
    --- a/src/include/basic/designer.bas
    +++ b/src/include/basic/designer.bas
    @@ -26,8 +26,8 @@
     ' properties window need them: their names, whether they hold other
     ' controls and can be placed by a designer, the name prefix of new
     ' controls, the default event, the toolbox tooltip and icon, and the
    -' properties, methods and events of each type.  "Form" (the form
    -' itself) and "Menu" (a menu item) answer for properties, methods and
    +' properties, methods, and events of each type.  "Form" (the form
    +' itself) and "Menu" (a menu item) answer for properties, methods, and
     ' events too, and "UserControl" (the object in a UserControl's own code)
     ' for events.  The answers are the ones the DVX BASIC IDE uses.
     '
    @@ -35,7 +35,7 @@
     ' CallByName (properties and methods by name), Form.Controls,
     ' Control.Parent / Container / MoveBefore, LoadFormText, and the design
     ' surface (Form.DesignMode, DesignSelection, the DesignMouse,
    -' DesignKey and DesignPropertyChanged events).
    +' DesignKey, and DesignPropertyChanged events).
     '
     ' A project's own UserControls (.ctl files) become control types of the
     ' designer with CompDesignControls (compiler.bas): they are then listed
    @@ -47,7 +47,7 @@
     ' again.
     '
     ' Form files as data (the .frm model, the one the DVX BASIC IDE loads,
    -' saves, copies and pastes with): FrmOpen / FrmOpenFile / FrmNew give a
    +' saves, copies, and pastes with): FrmOpen / FrmOpenFile / FrmNew give a
     ' handle (0: not a form); FrmGet$ / FrmSet read and change the form's
     ' properties, FrmCtl* its controls (numbered from 0, each control
     ' followed by its children; -1 means the form as a parent or "the end"
    @@ -116,7 +116,7 @@ DECLARE LIBRARY "basrt"
         ' ToolboxIcon file).  True when written.
         DECLARE FUNCTION CtlTypeIconFile(BYVAL typeName AS STRING, BYVAL outFile AS STRING) AS LONG
     
    -    ' The properties of a type: how many, and the name, type (CTL_PROP_)
    +    ' The properties of a type: how many, and the name, type (CTL_PROP_),
         ' and value names (an enumerated property's, "|"-separated in value
         ' order: Value 0 is the first) of property idx (0 to count - 1).
         DECLARE FUNCTION CtlPropCount(BYVAL typeName AS STRING) AS LONG
    @@ -135,12 +135,12 @@ DECLARE LIBRARY "basrt"
     
         ' The same for an event of one type: a UserControl's own events have
         ' parameters of their own, and so do the events of "UserControl" in
    -    ' its code (UserControl_MouseDown has Button, Shift, X and Y).
    +    ' its code (UserControl_MouseDown has Button, Shift, X, and Y).
         DECLARE FUNCTION CtlTypeEventParams$(BYVAL typeName AS STRING, BYVAL eventName AS STRING) AS STRING
     
         ' The text of .frm files and class modules, as the project and the
         ' Object list need them: the form's name, the names of the controls
    -    ' and menu items it declares ("|"-separated, in file order) and its
    +    ' and menu items it declares ("|"-separated, in file order), and its
         ' code (what follows the form's End line); a class module's name
         ' (its VB_Name, else the file name of path without the extension).
         DECLARE FUNCTION FrmFormName$(BYVAL frmText AS STRING) AS STRING
    @@ -172,7 +172,7 @@ DECLARE LIBRARY "basrt"
     
         ' The controls: how many, the number of control name (array member
         ' index, -1 for the first of that name; -1 when there is none), and
    -    ' control i's name, type, control array index (-1: not in an array)
    +    ' control i's name, type, control array index (-1: not in an array),
         ' and parent (-1: the form).
         DECLARE FUNCTION FrmCtlCount(BYVAL frm AS LONG) AS LONG
         DECLARE FUNCTION FrmCtlFind(BYVAL frm AS LONG, BYVAL ctlName AS STRING, BYVAL index AS LONG) AS LONG
    diff --git a/src/include/basic/fileops.bas b/src/include/basic/fileops.bas
    index c7afec38..07312264 100644
    --- a/src/include/basic/fileops.bas
    +++ b/src/include/basic/fileops.bas
    @@ -22,9 +22,9 @@
     
     
     ' fileops.bas -- File Operations Library for DVX BASIC
    -' Lists folders, works with paths, names and attributes, and copies, moves
    +' Lists folders, works with paths, names, and attributes, and copies, moves,
     ' and deletes files and whole folders -- at once, or as a job a program
    -' runs a little at a time with progress, Cancel and questions.  Lists
    +' runs a little at a time with progress, Cancel, and questions.  Lists
     ' drives and reports free disk space.  The single-file statements
     ' (FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR, SETATTR) are built in.
     ' Usage: Add this file to your project.
    @@ -103,7 +103,7 @@ DECLARE LIBRARY "fileops"
         ' The size in bytes of the disk holding path, or -1 when unknown.
         DECLARE FUNCTION FileOpsTotalBytes(BYVAL path AS STRING) AS DOUBLE
     
    -    ' What kind of drive drive is (its letter, as "C", "c:" or "C:\"): a
    +    ' What kind of drive drive is (its letter, as "C", "c:", or "C:\"): a
         ' FILEOPS_DRIVE_ value, or -1 when there is no such drive.  Asking
         ' never makes a floppy or CD-ROM drive spin up.
         DECLARE FUNCTION FileOpsDriveType(BYVAL drive AS STRING) AS LONG
    @@ -113,7 +113,7 @@ DECLARE LIBRARY "fileops"
         DECLARE FUNCTION FileOpsBootDrive$() AS STRING
     
         ' True when files can be made in folder (False on a CD-ROM, a write-
    -    ' protected floppy or a folder that does not exist).
    +    ' protected floppy, or a folder that does not exist).
         DECLARE FUNCTION FileOpsIsWritable(BYVAL folder AS STRING) AS LONG
     
         ' True when the drive holding path takes long file names (a long-name
    @@ -147,7 +147,7 @@ DECLARE LIBRARY "fileops"
         DECLARE FUNCTION FileOpsListCount(BYVAL list AS LONG) AS LONG
     
         ' Re-sort a listing: folders first, then by FILEOPS_SORT_NAME, _SIZE,
    -    ' _TYPE or _DATE (ties by name); descending True reverses each group.
    +    ' _TYPE, or _DATE (ties by name); descending True reverses each group.
         DECLARE SUB FileOpsListSort(BYVAL list AS LONG, BYVAL column AS LONG, BYVAL descending AS LONG)
     
         ' The number of the entry called itemName (case does not matter), or
    @@ -160,7 +160,7 @@ DECLARE LIBRARY "fileops"
         DECLARE FUNCTION FileOpsListSize(BYVAL list AS LONG, BYVAL index AS LONG) AS DOUBLE
     
         ' The whole listing as the rows of a file list, in its order: name,
    -    ' size ("1,234,567", blank for a folder), type ("Folder", "TXT File")
    +    ' size ("1,234,567", blank for a folder), type ("Folder", "TXT File"),
         ' and date ("YYYY-MM-DD HH:MM"), separated by tabs, one row per line
         ' -- exactly what a ListView's SetRows takes.
         DECLARE FUNCTION FileOpsListRows$(BYVAL list AS LONG) AS STRING
    @@ -187,7 +187,7 @@ DECLARE LIBRARY "fileops"
         ' True for a root such as "C:\" or "/".
         DECLARE FUNCTION FileOpsPathIsRoot(BYVAL path AS STRING) AS LONG
     
    -    ' True when path is complete by itself ("C:\DOCS", "\DOCS" or "/home"),
    +    ' True when path is complete by itself ("C:\DOCS", "\DOCS", or "/home"),
         ' False when it counts from the current folder ("DOCS\A.TXT").
         DECLARE FUNCTION FileOpsPathIsAbsolute(BYVAL path AS STRING) AS LONG
     
    @@ -252,7 +252,7 @@ DECLARE LIBRARY "fileops"
         DECLARE FUNCTION FileOpsIsFolder(BYVAL path AS STRING) AS LONG
     
         ' The FILEOPS_ATTR_ bits of a file or folder, or -1 when it cannot be
    -    ' read; set the Read-only, Hidden, System and Archive bits (True when
    +    ' read; set the Read-only, Hidden, System, and Archive bits (True when
         ' done).  Elsewhere than DOS, read-only means not writable and hidden
         ' a name starting with "."; only read-only can be set.
         DECLARE FUNCTION FileOpsGetAttr(BYVAL path AS STRING) AS LONG
    @@ -269,7 +269,7 @@ DECLARE LIBRARY "fileops"
         DECLARE FUNCTION FileOpsSetModified(BYVAL path AS STRING, BYVAL stamp AS STRING) AS LONG
     
         ' Add up a file, or a folder and everything in it: True when every
    -    ' part could be read.  Then FileOpsTreeBytes, FileOpsTreeFiles and
    +    ' part could be read.  Then FileOpsTreeBytes, FileOpsTreeFiles, and
         ' FileOpsTreeFolders give the totals (the folder itself not counted).
         DECLARE FUNCTION FileOpsTreeSize(BYVAL path AS STRING) AS LONG
         DECLARE FUNCTION FileOpsTreeBytes() AS DOUBLE
    @@ -279,9 +279,9 @@ DECLARE LIBRARY "fileops"
         ' A byte count with thousands separators, "1,234,567".
         DECLARE FUNCTION FileOpsFormatSize$(BYVAL bytes AS DOUBLE) AS STRING
     
    -    ' ---- Jobs: copy, move or delete with progress and questions ----
    +    ' ---- Jobs: copy, move, or delete with progress and questions ----
     
    -    ' Start a FILEOPS_COPY, FILEOPS_MOVE (into destFolder) or
    +    ' Start a FILEOPS_COPY, FILEOPS_MOVE (into destFolder), or
         ' FILEOPS_DELETE job.  Returns a job handle, 0 when out of memory.
         DECLARE FUNCTION FileOpsJobCreate(BYVAL op AS LONG, BYVAL destFolder AS STRING) AS LONG
     
    diff --git a/src/include/basic/font.bas b/src/include/basic/font.bas
    new file mode 100644
    index 00000000..be256e15
    --- /dev/null
    +++ b/src/include/basic/font.bas
    @@ -0,0 +1,107 @@
    +' The MIT License (MIT)
    +'
    +' Copyright (C) 2026 Scott Duensing
    +'
    +' Permission is hereby granted, free of charge, to any person obtaining a copy
    +' of this software and associated documentation files (the "Software"), to
    +' deal in the Software without restriction, including without limitation the
    +' rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +' sell copies of the Software, and to permit persons to whom the Software is
    +' furnished to do so, subject to the following conditions:
    +'
    +' The above copyright notice and this permission notice shall be included in
    +' all copies or substantial portions of the Software.
    +'
    +' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +' IN THE SOFTWARE.
    +
    +
    +' font.bas -- Fonts for DVX BASIC
    +'
    +' The fonts DVX has: "System" (the 8x16 font everything is drawn with
    +' unless told otherwise) and the families in DVX's FONTS folder, such as
    +' "DejaVu Sans", "Arimo", or "Fixed". A family comes in up to four styles
    +' (regular, bold, italic, bold italic) and in the pixel sizes it was
    +' made at; a font is drawn at the size nearest the one asked for, never
    +' scaled. A style a family has no face for is made from the nearest one:
    +' bold by thickening, italic by slanting. FontNearestSize tells which
    +' size you get.
    +'
    +' Name a font by family, size, and style on each call. A family name that
    +' is not there means the system font. Style is 0 (regular) or the sum of
    +' FONT_BOLD and FONT_ITALIC; FONT_UNDERLINE and FONT_STRIKE are lines
    +' drawn with the text and do not change a font's size.
    +'
    +'   FOR i = 0 TO FontFamilyCount() - 1
    +'       PRINT FontFamilyName$(i)
    +'   NEXT i
    +'   w = FontTextWidth("DejaVu Sans", 14, FONT_BOLD, "Hello")
    +'
    +' Usage: Add this file to your project.
    +
    +CONST FONT_BOLD      = 1
    +CONST FONT_ITALIC    = 2
    +CONST FONT_UNDERLINE = 4
    +CONST FONT_STRIKE    = 8
    +
    +DECLARE LIBRARY "libdvx"
    +
    +    ' How many font families there are ("System" is always the first).
    +    DECLARE FUNCTION FontFamilyCount() AS LONG
    +
    +    ' Family index, 0 to FontFamilyCount() - 1, in alphabetical order
    +    ' after "System" ("" out of range).
    +    DECLARE FUNCTION FontFamilyName$(BYVAL index AS LONG) AS STRING
    +
    +    ' True when every character of family is as wide as every other (a
    +    ' fixed-pitch font, for columns of text and code).
    +    DECLARE FUNCTION FontFamilyFixed(BYVAL family AS STRING) AS LONG
    +
    +    ' How many sizes family comes in for style (0 when there is no such
    +    ' family).
    +    DECLARE FUNCTION FontSizeCount(BYVAL family AS STRING, BYVAL style AS LONG) AS LONG
    +
    +    ' Size index, 0 to FontSizeCount - 1, in pixels, smallest first (0 out
    +    ' of range).
    +    DECLARE FUNCTION FontSizeAt(BYVAL family AS STRING, BYVAL style AS LONG, BYVAL index AS LONG) AS LONG
    +
    +    ' The size the font is drawn at when size is asked for.
    +    DECLARE FUNCTION FontNearestSize(BYVAL family AS STRING, BYVAL size AS LONG, BYVAL style AS LONG) AS LONG
    +
    +    ' The width of text in pixels when drawn in the font.
    +    DECLARE FUNCTION FontTextWidth(BYVAL family AS STRING, BYVAL size AS LONG, BYVAL style AS LONG, BYVAL text AS STRING) AS LONG
    +
    +    ' The height of one line of the font in pixels.
    +    DECLARE FUNCTION FontLineHeight(BYVAL family AS STRING, BYVAL size AS LONG, BYVAL style AS LONG) AS LONG
    +
    +    ' Pixels from the top of a line to the baseline the letters sit on.
    +    DECLARE FUNCTION FontAscent(BYVAL family AS STRING, BYVAL size AS LONG, BYVAL style AS LONG) AS LONG
    +
    +    ' Read the FONTS folder again (after fonts were added or removed).
    +    DECLARE SUB FontRescan()
    +
    +    ' The notice and licence that go with the font: the family, what it
    +    ' was converted from, its copyright notices, and its licence text (""
    +    ' for the system font).
    +    DECLARE FUNCTION FontLicence$(BYVAL family AS STRING, BYVAL size AS LONG, BYVAL style AS LONG) AS STRING
    +
    +    ' Add a .DFN font file for this program alone. Its family then comes
    +    ' first for this program: where it is installed too, the sizes and
    +    ' styles this font has replace the installed ones, and the installed
    +    ' family fills in the rest. Fonts in the program's own file (font
    +    ' resources, a project's Fonts) are added when it starts. True when
    +    ' the font was added.
    +    DECLARE FUNCTION FontAddFile(BYVAL path AS STRING) AS LONG
    +
    +    ' The same for a .DFN font held in bytes; label names it in DVX's log.
    +    DECLARE FUNCTION FontAddData(BYVAL label AS STRING, BYVAL bytes AS BINARY STRING) AS LONG
    +
    +    ' Add every font resource of another program or resource file for
    +    ' this program; how many were added.
    +    DECLARE FUNCTION FontAddResources(BYVAL path AS STRING) AS LONG
    +END DECLARE
    diff --git a/src/include/basic/hlpfile.bas b/src/include/basic/hlpfile.bas
    index 6aec87d3..fc32ccbf 100644
    --- a/src/include/basic/hlpfile.bas
    +++ b/src/include/basic/hlpfile.bas
    @@ -29,14 +29,14 @@
     ' show a topic in a HelpPane control (HelpPane1.ShowTopic handle, topic).
     '
     ' A help file is a handle (0 means it could not be opened). Topics,
    -' table of contents entries, index entries, search results and records
    +' table of contents entries, index entries, search results, and records
     ' are numbered from 0. Close each handle with HlpClose; whatever a
     ' program leaves open is closed when it ends.
     '
     ' Programs that draw topics themselves (instead of using HelpPane) read
     ' a topic's records with HlpLoadTopic and the HlpRecord functions: each
     ' record is one heading, paragraph, list item, note, code block, table,
    -' link, picture or rule (HLP_REC_ constants below), in order.
    +' link, picture, or rule (HLP_REC_ constants below), in order.
     '
     ' Usage: Add this file to your project.
     
    @@ -67,7 +67,7 @@ DECLARE LIBRARY "hlpfile"
     
         ' The table of contents: how many entries, and each entry's title,
         ' topic (-1 for a heading that groups others and opens nothing),
    -    ' depth (0 at the top, 1 under that, ...) and whether it starts
    +    ' depth (0 at the top, 1 under that, ...), and whether it starts
         ' expanded (True / False).
         DECLARE FUNCTION HlpTocCount(BYVAL handle AS LONG) AS LONG
         DECLARE FUNCTION HlpTocTitle$(BYVAL handle AS LONG, BYVAL entry AS LONG) AS STRING
    @@ -104,7 +104,7 @@ DECLARE LIBRARY "hlpfile"
         DECLARE FUNCTION HlpRecordLink$(BYVAL handle AS LONG, BYVAL record AS LONG) AS STRING
     
         ' Save a picture record to a file, for LOADPICTURE: the picture as it
    -    ' was put in the help file (a BMP, PNG, JPEG or GIF file; LOADPICTURE
    +    ' was put in the help file (a BMP, PNG, JPEG, or GIF file; LOADPICTURE
         ' reads any of them whatever the file is called). Returns True when
         ' the file was written.
         DECLARE FUNCTION HlpRecordImageSave(BYVAL handle AS LONG, BYVAL record AS LONG, BYVAL path AS STRING) AS LONG
    diff --git a/src/include/basic/joystick.bas b/src/include/basic/joystick.bas
    index b61b566d..b37a69cb 100644
    --- a/src/include/basic/joystick.bas
    +++ b/src/include/basic/joystick.bas
    @@ -23,18 +23,18 @@
     ' joystick.bas -- Joystick Library for DVX BASIC
     '
     ' Reads joysticks and game pads.  Joysticks are numbered from 1 to
    -' JoyCount(); their axes, buttons and hats are numbered from 1 too.  An
    +' JoyCount(); their axes, buttons, and hats are numbered from 1 too.  An
     ' axis reads from -32768 (all the way left or up) to 32767 (all the way
     ' right or down), 0 in the middle.  When no joystick is connected the
     ' keyboard can stand in for one (the arrow keys and Left Ctrl, Space,
    -' Left Shift and Z as buttons; the Control Panel's Game Controllers page
    +' Left Shift, and Z as buttons; the Control Panel's Game Controllers page
     ' changes the keys); it only answers while one of your program's windows
     ' is active.  Calibrate joysticks in the Control Panel.
     '
     ' The second half is for a program that sets joysticks up, as the
     ' Control Panel's Game Controllers page does: raw readings and
     ' calibration, dead zones, the game port's mode, the keyboard joystick's
    -' keys and the other [joystick] settings of DVX.INI.  Changes apply at
    +' keys, and the other [joystick] settings of DVX.INI.  Changes apply at
     ' once; JoySettingsStore puts them all into a settings handle from
     ' PrefsLoad (prefs.bas) for the program to save, and JoySettingsLoad
     ' takes them back from one (to undo).  Lists (choices, keys, keyboard
    @@ -65,7 +65,7 @@ CONST JOY_DEADZONE_DEFAULT = 8
     CONST JOY_DEADZONE_MAX     = 50
     
     ' How often joysticks are read (times a second): the JoySetting$ key,
    -' the usual rate and the slowest and fastest allowed
    +' the usual rate, and the slowest and fastest allowed
     CONST JOY_KEY_POLL_HZ     = "pollHz"
     CONST JOY_POLL_HZ_DEFAULT = 60
     CONST JOY_POLL_HZ_MIN     = 10
    @@ -122,7 +122,7 @@ DECLARE LIBRARY "joystick"
         ' while one of your program's windows is active).
         DECLARE FUNCTION JoyIsKeyboard(BYVAL joy AS LONG) AS LONG
     
    -    ' An axis's calibration in raw units: part JOY_CAL_MIN, JOY_CAL_CENTER
    +    ' An axis's calibration in raw units: part JOY_CAL_MIN, JOY_CAL_CENTER,
         ' or JOY_CAL_MAX of what was set, or of the range the axis found by
         ' itself (0 when the joystick cannot be calibrated).
         DECLARE FUNCTION JoyCalibration(BYVAL joy AS LONG, BYVAL axis AS LONG, BYVAL part AS LONG) AS LONG
    @@ -134,7 +134,7 @@ DECLARE LIBRARY "joystick"
         DECLARE FUNCTION JoyRawButtons(BYVAL joy AS LONG) AS LONG
     
         ' Calibrate an axis: the raw readings all the way one way, in the
    -    ' middle and all the way the other way (minV < centerV < maxV).
    +    ' middle, and all the way the other way (minV < centerV < maxV).
         ' True when it was taken.
         DECLARE FUNCTION JoySetCalibration(BYVAL joy AS LONG, BYVAL axis AS LONG, BYVAL minV AS LONG, BYVAL centerV AS LONG, BYVAL maxV AS LONG) AS LONG
     
    @@ -168,7 +168,7 @@ DECLARE LIBRARY "joystick"
         DECLARE FUNCTION JoyKeyNameOf$(BYVAL rawCode AS LONG) AS STRING
     
         ' The keyboard joystick's keys: slots 0 to JoyKbdSlotCount() - 1 are
    -    ' up, down, left, right and buttons 1 to 4.  JoyKbdSlot gives the
    +    ' up, down, left, right, and buttons 1 to 4.  JoyKbdSlot gives the
         ' JoyKeyName$ number of a slot's key; JoySetKbdSlot changes it (True
         ' when it was taken).
         DECLARE FUNCTION JoyKbdSlotCount() AS LONG
    diff --git a/src/include/basic/layout.bas b/src/include/basic/layout.bas
    index 19a7dcbc..9e54d4a0 100644
    --- a/src/include/basic/layout.bas
    +++ b/src/include/basic/layout.bas
    @@ -22,7 +22,7 @@
     
     ' layout.bas -- Sizes in characters and percent for DVX BASIC
     '
    -' A control's MinWidth, MinHeight, MaxWidth and MaxHeight (and Width and
    +' A control's MinWidth, MinHeight, MaxWidth, and MaxHeight (and Width and
     ' Height, which set the minimum) are pixels. To size a control by its
     ' text instead -- a TextBox wide enough for 16 characters, whatever the
     ' screen -- give them a size made here; the layout turns it into pixels
    diff --git a/src/include/basic/listhelp.bas b/src/include/basic/listhelp.bas
    index aa636b66..cc393728 100644
    --- a/src/include/basic/listhelp.bas
    +++ b/src/include/basic/listhelp.bas
    @@ -24,26 +24,26 @@
     '
     ' The rules the ListBox and ListView controls follow, for lists and icon
     ' views you draw yourself (a UserControl, a PictureBox): which items a
    -' click, Shift+click or Ctrl+click selects, which item the arrow keys,
    -' Home, End, PgUp and PgDn move to (in a list or a grid of icons), Ctrl+A
    +' click, Shift+click, or Ctrl+click selects, which item the arrow keys,
    +' Home, End, PgUp, and PgDn move to (in a list or a grid of icons), Ctrl+A
     ' and Space, type-ahead (pressing a letter goes to the next item starting
    -' with it) and where a dragged item lands. Your list then works exactly
    +' with it), and where a dragged item lands. Your list then works exactly
     ' like the built-in ones.
     '
     ' For icon views it also has Program Manager's rules (ListIcon...): the
    -' cells of the large icon, small icon and list views and where an item's
    +' cells of the large icon, small icon, and list views and where an item's
     ' picture and name go in them, the grid inside a sunken frame with a
     ' scroll bar only when the rows do not fit, the two lines of a large
     ' icon's name, the arrow keys among items placed in cells of their own,
    -' the scroll bar's arrows, trough and thumb, when a press starts a drag,
    +' the scroll bar's arrows, trough, and thumb, when a press starts a drag,
     ' the scrolling and the cell under a dragged item, and where a dropped
     ' item lands. An icon view drawn with them looks and behaves like Program
     ' Manager's.
     '
     ' A selection is a handle (0 means it could not be made) that remembers
    -' how many items there are, which are selected and which one is current
    +' how many items there are, which are selected, and which one is current
     ' (the one the keyboard moves). Items are numbered from 0. Tell it when
    -' items are added, removed or moved (ListSelInsert, ListSelRemove,
    +' items are added, removed, or moved (ListSelInsert, ListSelRemove,
     ' ListSelReorder) or all replaced (ListSelReset). Free it with ListSelFree;
     ' whatever a program leaves open is freed when it ends.
     '
    @@ -88,7 +88,7 @@ DECLARE LIBRARY "listhelp"
         ' (LISTSEL_KEY_TOGGLE). Any other key: LISTSEL_KEY_NONE, nothing done.
         DECLARE FUNCTION ListSelKey(BYVAL handle AS LONG, BYVAL keyCode AS LONG, BYVAL shift AS LONG) AS LONG
     
    -    ' A navigation key: the arrows, Home, End, PgUp or PgDn move the
    +    ' A navigation key: the arrows, Home, End, PgUp, or PgDn move the
         ' current item, in a list (columns = 1) or in a grid with columns
         ' items per row; pageRows rows are one page. With Shift the items from
         ' the last click up to the new current item are selected. Returns the
    @@ -132,7 +132,7 @@ DECLARE LIBRARY "listhelp"
         ' index it moves to, or -1 when the drop changes nothing.
         DECLARE FUNCTION ListReorderDest(BYVAL fromIndex AS LONG, BYVAL toSlot AS LONG, BYVAL count AS LONG) AS LONG
     
    -    ' Without a selection handle: the item Up, Down, Home, End, PgUp or
    +    ' Without a selection handle: the item Up, Down, Home, End, PgUp, or
         ' PgDn moves to from current in a list of count items (pageSize items
         ' a page), or in a grid with columns items per row (where Left and
         ' Right work too); -1 for any other key.
    @@ -148,7 +148,7 @@ DECLARE LIBRARY "listhelp"
         DECLARE FUNCTION ListTypeAheadMatch(BYVAL ch AS STRING, BYVAL text AS STRING) AS LONG
     
         ' Icon views. The grid functions take the view mode (LISTICON_VIEW_LARGE,
    -    ' LISTICON_VIEW_SMALL or LISTICON_VIEW_LIST), the size of a character
    +    ' LISTICON_VIEW_SMALL, or LISTICON_VIEW_LIST), the size of a character
         ' (TextWidth("W"), TextHeight("W")), the size of the whole view with
         ' its frame (width, height), how many items it holds, and rowsTaken:
         ' LISTICON_BY_ORDER when the items fill the grid in order, else how
    @@ -169,7 +169,7 @@ DECLARE LIBRARY "listhelp"
         ' "" for a line the name does not need.
         DECLARE FUNCTION ListIconLabel$(BYVAL itemName AS STRING, BYVAL maxChars AS LONG, BYVAL lineNo AS LONG) AS STRING
     
    -    ' The item an arrow key, Home, End, PgUp or PgDn moves to from current
    +    ' The item an arrow key, Home, End, PgUp, or PgDn moves to from current
         ' when the items stand in cells of their own, gaps allowed: cells holds
         ' each item's column and row, all separated by commas ("0,0,2,0,1,1").
         ' columns: the grid's (LISTICON_COLUMNS); pageRows: rows a page. -1
    diff --git a/src/include/basic/music.bas b/src/include/basic/music.bas
    index 5524e421..eae6b703 100644
    --- a/src/include/basic/music.bas
    +++ b/src/include/basic/music.bas
    @@ -25,7 +25,7 @@
     ' Plays music through whatever sound hardware the computer has.  Two
     ' kinds of music files:
     '
    -'   Tracker modules (MOD, S3M, XM and IT files) play wherever the computer
    +'   Tracker modules (MOD, S3M, XM, and IT files) play wherever the computer
     '   can play sampled sound.
     '   MIDI files (.MID, type 0 and 1) play on the computer's music
     '   synthesizer, or on a built-in synthesizer that sounds like it when
    @@ -33,7 +33,7 @@
     '
     ' You can also play notes yourself, like a keyboard: open a live music
     ' with MusicMidiOpen and send it notes.  MIDI channels are 1 to 16 (10 is
    -' the drums), notes, velocities (how hard a key is struck), instruments
    +' the drums), notes, velocities (how hard a key is struck), instruments,
     ' and controller values 0 to 127 (middle C is note 60).
     '
     ' MusicLoad and MusicMidiOpen return a music number for the other
    @@ -65,7 +65,7 @@ DECLARE LIBRARY "music"
         DECLARE SUB MusicPause(BYVAL music AS LONG)
         DECLARE SUB MusicResume(BYVAL music AS LONG)
     
    -    ' True while the music is playing (not stopped, finished or paused).
    +    ' True while the music is playing (not stopped, finished, or paused).
         DECLARE FUNCTION MusicPlaying(BYVAL music AS LONG) AS LONG
     
         ' Set the volume of this music (percent).
    @@ -89,8 +89,8 @@ DECLARE LIBRARY "music"
     
         ' About the music, by name (any case): "title" (a module's own
         ' title), "output" (where it plays, for people, like "OPL3 FM
    -    ' synthesizer at 388h"), "kind" ("module", "midi" or "live"), "where"
    -    ' ("stream", "mpu401", "fm" or "software"; "" before it first plays),
    +    ' synthesizer at 388h"), "kind" ("module", "midi", or "live"), "where"
    +    ' ("stream", "mpu401", "fm", or "software"; "" before it first plays),
         ' "paused" and "looping" (True or False as text: VAL gives the
         ' value).
         DECLARE FUNCTION MusicInfo$(BYVAL music AS LONG, BYVAL field AS STRING) AS STRING
    diff --git a/src/include/basic/prefs.bas b/src/include/basic/prefs.bas
    index 34ce8345..f32314f7 100644
    --- a/src/include/basic/prefs.bas
    +++ b/src/include/basic/prefs.bas
    @@ -44,12 +44,12 @@
     '
     ' Handles are also what other libraries take to read or store their
     ' settings: system.bas (colors, mouse, wallpaper), joystick.bas, net.bas,
    -' print.bas and sound.bas. PrefsDvxIni$ is the path of DVX's own settings
    +' print.bas, and sound.bas. PrefsDvxIni$ is the path of DVX's own settings
     ' file, DVX.INI, which is what Control Panel changes.
     '
     ' The choice lists are the settings DVX.INI stores by name (mouse
     ' acceleration, wallpaper mode, scroll wheel direction): for each choice
    -' a label to show in a list, the name stored in the file and the value
    +' a label to show in a list, the name stored in the file, and the value
     ' it stands for. The numbers below are the library's own and must not
     ' change.
     '
    @@ -75,7 +75,7 @@ DECLARE LIBRARY "libdvx"
         ' The number in key of [section], or defaultValue.
         DECLARE FUNCTION PrefsGetInt(BYVAL handle AS LONG, BYVAL section AS STRING, BYVAL key AS STRING, BYVAL defaultValue AS LONG) AS LONG
     
    -    ' True for "true", "yes" or "1", False for "false", "no" or "0" (in
    +    ' True for "true", "yes", or "1", False for "false", "no", or "0" (in
         ' any case); defaultValue for anything else or a missing key.
         DECLARE FUNCTION PrefsGetBool(BYVAL handle AS LONG, BYVAL section AS STRING, BYVAL key AS STRING, BYVAL defaultValue AS LONG) AS LONG
     
    @@ -108,11 +108,11 @@ DECLARE LIBRARY "libdvx"
         ' The path of DVX's own settings file (DVX.INI).
         DECLARE FUNCTION PrefsDvxIni$() AS STRING
     
    -    ' Store the screen mode DVX starts in ([video] width, height and bits
    +    ' Store the screen mode DVX starts in ([video] width, height, and bits
         ' per pixel).
         DECLARE SUB PrefsSetVideoMode(BYVAL handle AS LONG, BYVAL wide AS LONG, BYVAL high AS LONG, BYVAL bpp AS LONG)
     
    -    ' The screen mode stored in [video]: width, height and bits per pixel;
    +    ' The screen mode stored in [video]: width, height, and bits per pixel;
         ' DVX's own start-up mode for a value that is not there.
         DECLARE FUNCTION PrefsVideoWidth(BYVAL handle AS LONG) AS LONG
         DECLARE FUNCTION PrefsVideoHeight(BYVAL handle AS LONG) AS LONG
    diff --git a/src/include/basic/print.bas b/src/include/basic/print.bas
    index b6a067d6..e42cd0c4 100644
    --- a/src/include/basic/print.bas
    +++ b/src/include/basic/print.bas
    @@ -23,7 +23,7 @@
     ' print.bas -- Printing Library for DVX BASIC
     ' Prints text (with the page setup: margins, header, footer, page numbers)
     ' and pictures on the printer chosen in the Control Panel, with or without
    -' the common Print dialog.  LPRINT, LPRINT USING and OPEN "LPT1:" are built
    +' the common Print dialog.  LPRINT, LPRINT USING, and OPEN "LPT1:" are built
     ' in and need nothing from this file. It also reads and changes the
     ' printer settings (model, where the printer is connected) the way the
     ' Control Panel does; those calls also need prefs.bas.
    @@ -40,7 +40,7 @@ DECLARE LIBRARY "print"
         DECLARE FUNCTION PrintText(BYVAL text AS STRING, BYVAL title AS STRING) AS LONG
     
         ' Show the Print dialog, then print the picture file path$ (BMP, PNG,
    -    ' JPEG or GIF) scaled to fit the page.  Needs a printer model that
    +    ' JPEG, or GIF) scaled to fit the page.  Needs a printer model that
         ' prints pictures (see PrintCanPrintPictures).
         DECLARE FUNCTION PrintPictureDialog(BYVAL path AS STRING, BYVAL title AS STRING) AS LONG
     
    @@ -72,7 +72,7 @@ DECLARE LIBRARY "print"
         ' name).
         DECLARE FUNCTION PrintDestIsFile(BYVAL dest AS LONG) AS LONG
     
    -    ' The printer model, destination and file name in the settings of
    +    ' The printer model, destination, and file name in the settings of
         ' prefs (the usual settings when they are missing).
         DECLARE FUNCTION PrintSettingModel(BYVAL prefs AS LONG) AS LONG
         DECLARE FUNCTION PrintSettingDest(BYVAL prefs AS LONG) AS LONG
    @@ -82,7 +82,7 @@ DECLARE LIBRARY "print"
         ' (printing to a file needs its name).
         DECLARE FUNCTION PrintSettingsProblem$(BYVAL dest AS LONG, BYVAL filePath AS STRING) AS STRING
     
    -    ' Put the model, destination and file name into the settings of
    +    ' Put the model, destination, and file name into the settings of
         ' prefs, keeping the page setup as it is saved now; save prefs
         ' afterwards (PrefsSave). True when done, False for a bad handle or
         ' a number out of range.
    @@ -95,7 +95,7 @@ DECLARE LIBRARY "print"
     
         ' A number of the page setup in prefs: "Lines" (lines per page, 0 =
         ' the printer's usual page length), "Columns", "MarginTop",
    -    ' "MarginBottom", "MarginLeft", "MarginRight", "TabWidth" or "Wrap"
    +    ' "MarginBottom", "MarginLeft", "MarginRight", "TabWidth", or "Wrap"
         ' (True: long lines wrap at a space, False: they are cut off); -1 for
         ' an unknown field.
         DECLARE FUNCTION PrintPageSetting(BYVAL prefs AS LONG, BYVAL field AS STRING) AS LONG
    diff --git a/src/include/basic/resource.bas b/src/include/basic/resource.bas
    index c997605c..eb1a152b 100644
    --- a/src/include/basic/resource.bas
    +++ b/src/include/basic/resource.bas
    @@ -24,13 +24,15 @@
     '
     ' Provides access to DVX resource blocks appended to binary
     ' files (.app, .wgt, .lib). Resources are named data entries
    -' of type icon, text, or binary.
    +' of type icon, text, binary, or font (a .DFN font the program carries:
    +' DVX adds it for the program when the program starts).
     '
     ' Usage: Add this file to your project.
     
     CONST RES_TYPE_ICON   = 1
     CONST RES_TYPE_TEXT   = 2
     CONST RES_TYPE_BINARY = 3
    +CONST RES_TYPE_FONT   = 4
     
     ' Names DVX gives the icon resources, and the longest name plus one
     CONST RES_NAME_ICON        = "icon32"
    @@ -52,7 +54,8 @@ DECLARE LIBRARY "basrt"
         DECLARE FUNCTION ResName$(BYVAL handle AS LONG, BYVAL idx AS LONG) AS STRING
     
         ' Return the type of a resource by zero-based index.
    -    ' Returns RES_TYPE_ICON (1), RES_TYPE_TEXT (2), or RES_TYPE_BINARY (3).
    +    ' Returns RES_TYPE_ICON (1), RES_TYPE_TEXT (2), RES_TYPE_BINARY (3),
    +    ' or RES_TYPE_FONT (4).
         DECLARE FUNCTION ResType(BYVAL handle AS LONG, BYVAL idx AS LONG) AS LONG
     
         ' Return the size in bytes of a resource by zero-based index.
    @@ -74,7 +77,8 @@ DECLARE LIBRARY "basrt"
         DECLARE FUNCTION ResAddText(BYVAL path AS STRING, BYVAL resName AS STRING, BYVAL text AS STRING) AS LONG
     
         ' Add or replace a resource from a file. Type should be
    -    ' RES_TYPE_ICON or RES_TYPE_BINARY. Returns True on success.
    +    ' RES_TYPE_ICON, RES_TYPE_BINARY, or RES_TYPE_FONT. Returns True on
    +    ' success.
         DECLARE FUNCTION ResAddFile(BYVAL path AS STRING, BYVAL resName AS STRING, BYVAL resType AS LONG, BYVAL srcFile AS STRING) AS LONG
     
         ' Remove a resource by name. Returns True on success.
    diff --git a/src/include/basic/richtext.bas b/src/include/basic/richtext.bas
    new file mode 100644
    index 00000000..b20ae002
    --- /dev/null
    +++ b/src/include/basic/richtext.bas
    @@ -0,0 +1,41 @@
    +' The MIT License (MIT)
    +'
    +' Copyright (C) 2026 Scott Duensing
    +'
    +' Permission is hereby granted, free of charge, to any person obtaining a copy
    +' of this software and associated documentation files (the "Software"), to
    +' deal in the Software without restriction, including without limitation the
    +' rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +' sell copies of the Software, and to permit persons to whom the Software is
    +' furnished to do so, subject to the following conditions:
    +'
    +' The above copyright notice and this permission notice shall be included in
    +' all copies or substantial portions of the Software.
    +'
    +' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +' IN THE SOFTWARE.
    +
    +
    +' richtext.bas -- RTF for DVX BASIC
    +'
    +' RTF (Rich Text Format) is how word processors save text with fonts,
    +' sizes, bold, italic, colors, and paragraph alignment. A RichEdit
    +' control reads and writes it whole (TextRTF, SelRTF, LoadFile, and
    +' SaveFile); with this file a program can also turn RTF into plain text
    +' and plain text into RTF without a control.
    +' Usage: Add this file to your project.
    +
    +DECLARE LIBRARY "richtext"
    +    ' The text of an RTF document, one line per paragraph, without its
    +    ' fonts and styles. Text that is not RTF comes back as it is.
    +    DECLARE FUNCTION RtfToText$(BYVAL rtf AS STRING) AS STRING
    +
    +    ' Plain text as an RTF document in the system font, a paragraph per
    +    ' line (tabs become spaces).
    +    DECLARE FUNCTION TextToRtf$(BYVAL text AS STRING) AS STRING
    +END DECLARE
    diff --git a/src/include/basic/security.bas b/src/include/basic/security.bas
    index 6ccb0a74..3ec92e1f 100644
    --- a/src/include/basic/security.bas
    +++ b/src/include/basic/security.bas
    @@ -25,11 +25,11 @@
     ' The building blocks the encrypted serial link uses, for your own
     ' protocols and files: random bytes for keys, a key exchange (two
     ' programs agree on a secret over a line anyone can read), a cipher,
    -' SHA-256 hashes, HMAC signatures, key stretching (HKDF) and a compare
    +' SHA-256 hashes, HMAC signatures, key stretching (HKDF), and a compare
     ' that does not give away where two codes differ.
     '
    -' Keys, data and results are strings that may hold any bytes, CHR$(0)
    -' too. A key exchange, cipher, hash or HMAC in progress is a handle (0
    +' Keys, data, and results are strings that may hold any bytes, CHR$(0)
    +' too. A key exchange, cipher, hash, or HMAC in progress is a handle (0
     ' means it could not be made); free it with its Free call (SecSha256End$
     ' and SecHmacEnd$ free theirs). Whatever a program leaves open is freed
     ' when it ends.
    diff --git a/src/include/basic/shell.bas b/src/include/basic/shell.bas
    index 008c096c..2441c669 100644
    --- a/src/include/basic/shell.bas
    +++ b/src/include/basic/shell.bas
    @@ -27,12 +27,12 @@
     ' open documents, run command lines and the Run dialog, the installed
     ' programs with their names and descriptions, file types (which program
     ' opens which kind of document), the running programs (find one, bring
    -' it to the front, ask it to close or end it), command lines split into
    +' it to the front, ask it to close, or end it), command lines split into
     ' their parts, this program's priority, keys that work in every program,
     ' Task Manager, system information, the desktop and the program started
    -' after it, Exit DVX, this program's settings folder and the DVX log
    +' after it, Exit DVX, this program's settings folder, and the DVX log
     ' (DVX.LOG); and Program Manager's program groups (PROGMAN.INI) with
    -' their items, shortcut keys and icons.
    +' their items, shortcut keys, and icons.
     ' Usage: Add this file to your project.
     '
     ' "This program" is the program the code belongs to; when it runs in the
    @@ -198,7 +198,7 @@ DECLARE LIBRARY "dvxshell"
         ' is not a running program.
         DECLARE FUNCTION ShellAppMemory(BYVAL appId AS LONG) AS LONG
     
    -    ' A number that changes whenever a program starts, ends or is ended:
    +    ' A number that changes whenever a program starts, ends, or is ended:
         ' check it from a Timer and refresh what you show when it differs
         ' from the value you saw last.
         DECLARE FUNCTION ShellChangeCount() AS LONG
    @@ -243,7 +243,7 @@ DECLARE LIBRARY "dvxshell"
         ' ShellGroupsScan fills and which is never saved or changed by hand.
         DECLARE FUNCTION ShellGroupsLoad(BYVAL path AS STRING) AS LONG
     
    -    ' Save the groups, options and Run history (the old file is replaced
    +    ' Save the groups, options, and Run history (the old file is replaced
         ' only once the new one is complete).  True when saved.
         DECLARE FUNCTION ShellGroupsSave(BYVAL groups AS LONG, BYVAL path AS STRING) AS LONG
     
    @@ -305,7 +305,7 @@ DECLARE LIBRARY "dvxshell"
         ' its file arguments are looked for), "icon" (a picture file, "" for
         ' the program's own), "key" (its shortcut, "" for none), "minimized"
         ' ("1" to start it minimized, else "0"), "col" and "row" (its grid
    -    ' cell, -1 when not placed) and "description" (its tooltip).
    +    ' cell, -1 when not placed), and "description" (its tooltip).
         DECLARE FUNCTION ShellItemGet$(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL item AS LONG, BYVAL field AS STRING) AS STRING
     
         ' Change a field.  False for an unknown field, an empty name or
    @@ -323,7 +323,7 @@ DECLARE LIBRARY "dvxshell"
         DECLARE SUB ShellItemPlace(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL item AS LONG, BYVAL col AS LONG, BYVAL row AS LONG)
     
         ' True when searchText (any case; "" matches everything) is in the item's
    -    ' name, description or program file name: Program Manager's search.
    +    ' name, description, or program file name: Program Manager's search.
         DECLARE FUNCTION ShellItemMatches(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL item AS LONG, BYVAL searchText AS STRING) AS LONG
     
         ' True when keyText can be an item's shortcut key: Ctrl together with
    diff --git a/src/include/basic/sound.bas b/src/include/basic/sound.bas
    index 72b01a9d..a2cc0920 100644
    --- a/src/include/basic/sound.bas
    +++ b/src/include/basic/sound.bas
    @@ -34,11 +34,11 @@
     '
     ' The second half is for a program that changes DVX's own sound
     ' settings, as the Control Panel's Sounds page does: the sound DVX plays
    -' for each of its events, the master volume, the sound driver and where
    +' for each of its events, the master volume, the sound driver, and where
     ' MIDI music plays.  Those settings live in DVX.INI; the functions that
     ' read or write them take a handle from PrefsLoad (prefs.bas, with
     ' PrefsDvxIni$ for the file), and the program saves it with PrefsSave.
    -' Events, drivers and MIDI choices are numbered from 0, so they fill a
    +' Events, drivers, and MIDI choices are numbered from 0, so they fill a
     ' ListBox or DropDown in order.
     '
     ' Usage: Add this file to your project.
    @@ -122,7 +122,7 @@ DECLARE LIBRARY "audio"
         ' Play a WAV or VOC file over and over until StopWave.
         DECLARE FUNCTION PlayWaveLoop(BYVAL fileName AS STRING) AS LONG
     
    -    ' Stop a sound started by PlayWave, PlayWaveLoop, PlayTone or PlayNoise.
    +    ' Stop a sound started by PlayWave, PlayWaveLoop, PlayTone, or PlayNoise.
         DECLARE SUB StopWave(BYVAL voice AS LONG)
     
         ' Stop every sound this program is playing.
    @@ -190,7 +190,7 @@ DECLARE LIBRARY "audio"
     
         ' Change a voice while it plays: its volume (percent), its pan
         ' (-100 to 100), its pitch (percent of the recorded speed, 25 to
    -    ' 400) and a tone's frequency (hertz).
    +    ' 400), and a tone's frequency (hertz).
         DECLARE SUB SoundSetVolume(BYVAL voice AS LONG, BYVAL volume AS LONG)
         DECLARE SUB SoundSetPan(BYVAL voice AS LONG, BYVAL pan AS LONG)
         DECLARE SUB SoundSetPitch(BYVAL voice AS LONG, BYVAL percent AS LONG)
    @@ -211,7 +211,7 @@ DECLARE LIBRARY "audio"
         DECLARE FUNCTION SoundStreamSpace(BYVAL stream AS LONG) AS LONG
         DECLARE FUNCTION SoundStreamQueued(BYVAL stream AS LONG) AS LONG
     
    -    ' The stream's voice, for SoundSetVolume, SoundSetPan and
    +    ' The stream's voice, for SoundSetVolume, SoundSetPan, and
         ' SoundSetPitch.
         DECLARE FUNCTION SoundStreamVoice(BYVAL stream AS LONG) AS LONG
     
    @@ -227,7 +227,7 @@ DECLARE LIBRARY "audio"
         DECLARE FUNCTION FmInfo$(BYVAL field AS STRING) AS STRING
     
         ' Take a voice of the chip (0 when none is free), set its registers
    -    ' (FM_... above; True when it worked) and give it back.
    +    ' (FM_... above; True when it worked), and give it back.
         DECLARE FUNCTION FmAlloc() AS LONG
         DECLARE FUNCTION FmWrite(BYVAL voice AS LONG, BYVAL reg AS LONG, BYVAL value AS LONG) AS LONG
         DECLARE SUB FmFree(BYVAL voice AS LONG)
    @@ -286,13 +286,13 @@ DECLARE LIBRARY "audio"
         DECLARE FUNCTION SoundMidiLabel$(BYVAL index AS LONG) AS STRING
     
         ' The settings' sound driver (SoundDriverAuto$ when none is set),
    -    ' MIDI choice (an index for SoundMidiLabel$) and master volume
    +    ' MIDI choice (an index for SoundMidiLabel$), and master volume
         ' (percent).
         DECLARE FUNCTION SoundSettingDriver$(BYVAL prefs AS LONG) AS STRING
         DECLARE FUNCTION SoundSettingMidi(BYVAL prefs AS LONG) AS LONG
         DECLARE FUNCTION SoundSettingVolume(BYVAL prefs AS LONG) AS LONG
     
    -    ' Store the sound driver, MIDI choice and master volume in the
    +    ' Store the sound driver, MIDI choice, and master volume in the
         ' settings.  A new driver or MIDI choice takes effect when DVX next
         ' starts.
         DECLARE SUB SoundSettingsStore(BYVAL prefs AS LONG, BYVAL driver AS STRING, BYVAL midi AS LONG, BYVAL volume AS LONG)
    diff --git a/src/include/basic/system.bas b/src/include/basic/system.bas
    index d5c2fbfd..d02ecfad 100644
    --- a/src/include/basic/system.bas
    +++ b/src/include/basic/system.bas
    @@ -23,12 +23,13 @@
     
     ' system.bas -- System Settings Library for DVX BASIC
     ' Changes what Control Panel changes, for the whole desktop and at once:
    -' the screen mode, the system colors and color themes, the wallpaper and
    -' the mouse. Together with prefs.bas (DVX.INI), it lets a program save
    -' these settings so DVX starts with them. Also reads the system font's
    -' character pictures (glyphs), the memory, the mouse pointer and the
    -' Shift / Ctrl / Alt keys anywhere on the screen, moves the pointer,
    -' types keys into the window in front and saves the screen as a picture.
    +' the screen mode, the system colors and color themes, the wallpaper,
    +' text smoothing, and the mouse. Together with prefs.bas (DVX.INI), it
    +' lets a program save these settings so DVX starts with them. Also reads
    +' the system font's character pictures (glyphs), the memory, the mouse
    +' pointer, and the Shift / Ctrl / Alt keys anywhere on the screen, moves
    +' the pointer, types keys into the window in front, and saves the screen
    +' as a picture.
     ' Usage: Add this file and prefs.bas to your project.
     '
     ' Colors are RGB() values. Lists (screen modes, colors) are numbered
    @@ -37,9 +38,9 @@
     ' to save them for the next start, or a PrefsCreate handle as a snapshot
     ' in memory to go back to (what Cancel in Control Panel does).
     '
    -' The mouse's acceleration, scroll wheel direction and wallpaper mode
    +' The mouse's acceleration, scroll wheel direction, and wallpaper mode
     ' choices are the prefs.bas choice lists PREFS_CHOICE_ACCEL,
    -' PREFS_CHOICE_WHEEL and PREFS_CHOICE_WALLPAPER.
    +' PREFS_CHOICE_WHEEL, and PREFS_CHOICE_WALLPAPER.
     
     ' Mouse limits and DVX's defaults (SysSetMouse)
     CONST MOUSE_WHEEL_STEP_MIN      = 1      ' lines per wheel notch, least
    @@ -59,7 +60,7 @@ DECLARE LIBRARY "dvxshell"
         ' How many screen modes the display offers.
         DECLARE FUNCTION SysVideoModeCount() AS LONG
     
    -    ' Width, height and bits per pixel (8, 15, 16, 24 or 32) of mode
    +    ' Width, height, and bits per pixel (8, 15, 16, 24, or 32) of mode
         ' index; 0 when there is no such mode.
         DECLARE FUNCTION SysVideoModeWidth(BYVAL index AS LONG) AS LONG
         DECLARE FUNCTION SysVideoModeHeight(BYVAL index AS LONG) AS LONG
    @@ -115,7 +116,7 @@ DECLARE LIBRARY "dvxshell"
     
         ' ---- Wallpaper ----
     
    -    ' Show an image file (BMP, JPG, PNG or GIF) behind the desktop; ""
    +    ' Show an image file (BMP, JPG, PNG, or GIF) behind the desktop; ""
         ' for none. Returns True when it was loaded (False keeps the old one).
         DECLARE FUNCTION SysSetWallpaper(BYVAL path AS STRING) AS LONG
     
    @@ -135,6 +136,21 @@ DECLARE LIBRARY "dvxshell"
         ' loaded.
         DECLARE FUNCTION SysWallpaperLoad(BYVAL prefs AS LONG) AS LONG
     
    +    ' ---- Text ----
    +
    +    ' Smooth the edges of text (True) or not (False). Text is smoothed
    +    ' only when the screen shows more than 256 colors, in the fonts and
    +    ' sizes that come smoothed. On unless the user turned it off.
    +    DECLARE SUB SysSetTextSmoothing(BYVAL smooth AS LONG)
    +    DECLARE FUNCTION SysTextSmoothing() AS LONG
    +
    +    ' Store the text settings in [fonts] of a prefs.bas handle.
    +    DECLARE SUB SysFontsStore(BYVAL prefs AS LONG)
    +
    +    ' Use the [fonts] settings of a prefs.bas handle (smoothing on when
    +    ' it does not say).
    +    DECLARE SUB SysFontsLoad(BYVAL prefs AS LONG)
    +
         ' ---- Mouse ----
     
         ' Apply a mouse setup: wheelDir 1 normal or -1 reversed (a value of
    @@ -163,7 +179,7 @@ DECLARE LIBRARY "dvxshell"
         ' ---- The pointer and the keyboard, anywhere on the screen ----
     
         ' Where the mouse pointer is, in screen pixels from the top left
    -    ' corner, and the buttons held (vbLeftButton, vbRightButton and
    +    ' corner, and the buttons held (vbLeftButton, vbRightButton, and
         ' vbMiddleButton bits) -- also over other programs' windows.
         DECLARE FUNCTION SysMouseX() AS LONG
         DECLARE FUNCTION SysMouseY() AS LONG
    @@ -175,7 +191,7 @@ DECLARE LIBRARY "dvxshell"
         ' "click" a control, call its event procedure or set its value.
         DECLARE SUB SysSetMousePos(BYVAL x AS LONG, BYVAL y AS LONG)
     
    -    ' The Shift, Ctrl and Alt keys held now: vbShiftMask, vbCtrlMask and
    +    ' The Shift, Ctrl, and Alt keys held now: vbShiftMask, vbCtrlMask, and
         ' vbAltMask bits.
         DECLARE FUNCTION SysShiftState() AS LONG
     
    @@ -183,7 +199,7 @@ DECLARE LIBRARY "dvxshell"
         ' pressed it (like VB's SendKeys): keyCode as a KeyDown event gets it
         ' -- a character code such as ASC("a") or 13 for Enter, or a vbKey
         ' constant (vbKeyF1, vbKeyLeft ...) -- with shift the vbShiftMask,
    -    ' vbCtrlMask and vbAltMask bits held with it (SysSendKey ASC("s"),
    +    ' vbCtrlMask, and vbAltMask bits held with it (SysSendKey ASC("s"),
         ' vbCtrlMask is Ctrl+S). The keys arrive in order once your code has
         ' returned to DVX (the end of the event procedure), one at a time;
         ' a key for your own program's form arrives as the user's would.
    diff --git a/src/include/basic/xfer.bas b/src/include/basic/xfer.bas
    index d85e74c0..75062300 100644
    --- a/src/include/basic/xfer.bas
    +++ b/src/include/basic/xfer.bas
    @@ -22,7 +22,7 @@
     
     ' xfer.bas -- File Transfer Library for DVX BASIC
     '
    -' Sends and receives files with XMODEM, XMODEM-1K, YMODEM or ZMODEM. A
    +' Sends and receives files with XMODEM, XMODEM-1K, YMODEM, or ZMODEM. A
     ' transfer is a handle. XferSendFile and XferReceive start one over a
     ' serial port opened with SerOpen (from comm.bas); then call XferPoll
     ' often (from a Timer, for example) until it returns something other than
    diff --git a/src/installer/Makefile b/src/installer/Makefile
    index 69a4e61a..69f3439c 100644
    --- a/src/installer/Makefile
    +++ b/src/installer/Makefile
    @@ -23,10 +23,12 @@
     # The DVX installer.
     #
     #   bin/setup.exe     SETUP.EXE: one static DOS program -- the DVX core,
    -#                     the widgets Setup uses, fileops, miniz's inflate and
    -#                     the Setup program -- linked with --gc-sections so
    -#                     only what Setup reaches is in it.  Also shipped in
    -#                     the package (the installed copy uninstalls).
    +#                     the widgets Setup uses, fileops, miniz's inflate, the
    +#                     Setup program, and its fonts (setupFonts.ini) --
    +#                     linked with --gc-sections so only what Setup reaches
    +#                     is in it, then packed with UPX (it unpacks itself in
    +#                     memory when it starts).  Also shipped in the package
    +#                     (the installed copy uninstalls).
     #   bin/host/dvxpkg   the package tool `make dist` runs (src/installer/
     #                     mkDist.sh).
     #
    @@ -51,6 +53,14 @@ HOSTBIN    = ../../bin/host
     TARGET     = $(BINDIR)/setup.exe
     TOOL       = $(HOSTBIN)/dvxpkg
     LICENSE_H  = $(OBJDIR)/setupLicense.h
    +FONTS_H    = $(OBJDIR)/setupFonts.h
    +FONTS_INI  = setupFonts.ini
    +FONTS_DIR  = $(OBJDIR)/fonts
    +MKFONT     = $(HOSTBIN)/mkfont
    +# NRV2E: about half the size, and it unpacks in a fraction of a second on
    +# a 486 (LZMA saves 15 KB more but unpacks several times slower).  UPX
    +# keeps the CWSDPMI stub.
    +UPX_FLAGS  = -qq --best --nrv2e
     
     INCLUDES   = -I. -I$(MINIZ) -I$(OBJDIR) -I$(LIBDVX) -I$(LIBDVX)/platform -I$(LIBDVX)/thirdparty -I$(KPUNCH) -I$(KPUNCH)/libtasks -I$(WIDGETS)
     MINIZ_DOS  = -DMINIZ_NO_ARCHIVE_APIS -DMINIZ_NO_STDIO -DMINIZ_NO_TIME -DMINIZ_NO_DEFLATE_APIS -DMINIZ_NO_ZLIB_COMPATIBLE_NAMES
    @@ -62,7 +72,7 @@ SETUP_SRCS = setup.c setupCore.c pkgRead.c pkgFormat.c $(MINIZ)/miniz.c
     CORE_SRCS  = $(wildcard $(LIBDVX)/*.c) $(LIBDVX)/platform/dvxPlatformDos.c $(LIBDVX)/platform/dvxPlatformIrq.c $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/platform/dvxMemTrack.c $(LIBDVX)/thirdparty/stb_ds_impl.c ../tools/dvxResWrite.c $(wildcard $(KPUNCH)/fileops/*.c) $(KPUNCH)/texthelp/textHelp.c
     
     # The widgets Setup uses: /: (wgtRegister -> wgtRegister)
    -SETUP_WGTS = box/widgetBox:Box button/widgetButton:Button checkbox/widgetCheckbox:Checkbox label/widgetLabel:Label progressBar/widgetProgressBar:Progress spacer/widgetSpacer:Spacer textInput/widgetTextInput:TextInput
    +SETUP_WGTS = box/widgetBox:Box button/widgetButton:Button checkbox/widgetCheckbox:Checkbox label/widgetLabel:Label progressBar/widgetProgressBar:Progress radio/widgetRadio:Radio spacer/widgetSpacer:Spacer textInput/widgetTextInput:TextInput
     
     SETUP_OBJS = $(patsubst %.c,$(DOSOBJ)/%.o,$(notdir $(SETUP_SRCS)))
     CORE_OBJS  = $(patsubst %.c,$(DOSOBJ)/core/%.o,$(notdir $(CORE_SRCS)))
    @@ -78,6 +88,10 @@ vpath %.c . $(MINIZ)
     
     .PHONY: all clean
     
    +# A recipe that fails part way (UPX, after the stub is prepended) leaves
    +# no target behind to pass for a finished one
    +.DELETE_ON_ERROR:
    +
     all: $(TARGET) $(TOOL)
     
     $(TARGET): $(SETUP_OBJS) $(CORE_LIB) | $(BINDIR)
    @@ -85,12 +99,13 @@ $(TARGET): $(SETUP_OBJS) $(CORE_LIB) | $(BINDIR)
     	$(EXE2COFF) $@
     	cat $(CWSDSTUB) $(BINDIR)/setup > $@
     	rm -f $(BINDIR)/setup
    +	$(UPX) $(UPX_FLAGS) $@
     
     $(CORE_LIB): $(CORE_OBJS) $(WGT_OBJS)
     	rm -f $@
     	$(DJGPP_PREFIX)/bin/$(DJGPP_TRIPLE)-ar rcs $@ $^
     
    -$(DOSOBJ)/%.o: %.c $(DVX_MAKEFILES) | $(LICENSE_H)
    +$(DOSOBJ)/%.o: %.c $(DVX_MAKEFILES) | $(LICENSE_H) $(FONTS_H)
     	@mkdir -p $(dir $@)
     	$(CC) $(CFLAGS) -c -o $@ $<
     
    @@ -121,6 +136,28 @@ $(LICENSE_H): $(LICENSE) $(DVX_MAKEFILES)
     	  tr -d '\r' < $< | iconv -f UTF-8 -t ASCII//TRANSLIT | fmt -w $(LICENSE_COLS) | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/^/    "/' -e 's/$$/\\n"/'; \
     	  echo ';'; } > $@.tmp && mv $@.tmp $@
     
    +# The fonts Setup carries (setupFonts.ini), made by mkfont and written as
    +# C arrays, one per .DFN, and the table setup.c reads (sSetupFonts, of
    +# SetupFontDataT: name, bytes, and length)
    +$(FONTS_H): $(FONTS_INI) $(MKFONT) $(FONT_SOURCES)/.installed $(DVX_MAKEFILES)
    +	rm -rf $(FONTS_DIR)
    +	$(MKFONT) $(FONTS_INI) $(FONT_SOURCES) $(FONTS_DIR)
    +	{ echo '// Generated from setupFonts.ini by src/installer/Makefile'; \
    +	  echo '#include '; \
    +	  echo 'typedef struct { const char *name; const uint8_t *data; uint32_t length; } SetupFontDataT;'; \
    +	  for f in $(FONTS_DIR)/*.DFN; do \
    +	    n=$$(basename $$f .DFN); \
    +	    echo "static const uint8_t sFont$$n[] = {"; \
    +	    od -An -v -tx1 $$f | sed -e 's/  */ /g' -e 's/^ //' -e 's/ $$//' -e 's/ /, 0x/g' -e 's/^/    0x/' -e 's/$$/,/'; \
    +	    echo '};'; \
    +	  done; \
    +	  echo 'static const SetupFontDataT sSetupFonts[] = {'; \
    +	  for f in $(FONTS_DIR)/*.DFN; do \
    +	    n=$$(basename $$f .DFN); \
    +	    echo "    { \"$$n\", sFont$$n, (uint32_t)sizeof(sFont$$n) },"; \
    +	  done; \
    +	  echo '};'; } > $@.tmp && mv $@.tmp $@
    +
     $(TOOL): $(TOOL_OBJS) | $(HOSTBIN)
     	$(HOSTCC) -o $@ $(TOOL_OBJS)
     
    diff --git a/src/installer/README.TXT b/src/installer/README.TXT
    index fca02e79..bc730629 100644
    --- a/src/installer/README.TXT
    +++ b/src/installer/README.TXT
    @@ -2,7 +2,7 @@ DVX
     ===
     
     DVX is a graphical desktop for DOS: windows, a Program Manager, a File
    -Manager, a Notepad, a Terminal, DVX BASIC and more.
    +Manager, a Notepad, a Terminal, DVX BASIC, and more.
     
     
     What you need
    @@ -13,7 +13,8 @@ What you need
         buffer); a VESA driver such as UNIVBE works with older cards
       * 4 MB of extended memory (8 MB or more recommended)
       * A two-button mouse and its DOS driver
    -  * About 10 MB of free space on the hard disk
    +  * About 12 MB of free space on the hard disk (14 MB with the base set
    +    of fonts, 22 MB with all the fonts)
     
     
     Installing
    @@ -30,10 +31,19 @@ From floppy disks: put Disk 1 in drive A: and type
         SETUP
     
     Setup shows the licence, asks for the folder to install DVX in (C:\DVX
    -unless you change it), checks the free space and unpacks DVX. With
    -floppies it asks for each disk in turn. Setup can also add a line to
    -AUTOEXEC.BAT so DVX starts with the computer; it shows the line first and
    -keeps a copy of the old file as AUTOEXEC.DVX.
    +unless you change it) and which fonts to install, checks the free
    +space, and unpacks DVX. With floppies it asks for each disk it needs in
    +turn. Setup can also add a line to AUTOEXEC.BAT so DVX starts with the
    +computer; it shows the line first and keeps a copy of the old file as
    +AUTOEXEC.DVX.
    +
    +The fonts: None keeps DVX to its own font; the base set adds DejaVu
    +Sans, Sans Mono, and Serif in all their styles (about 2 MB); All adds
    +every font that comes with DVX (about 8 MB). A program that asks for a
    +font you did not install shows its text in DVX's own font, unless it
    +carries that font itself. Each font file holds the licence of the font
    +it was made from. With None or the base set, Setup does not ask for the
    +last floppy disks, which hold only fonts.
     
     Setup never writes to the CD or the floppy disks.
     
    @@ -43,7 +53,8 @@ Upgrading or repairing
     
     Run Setup again and choose the same folder. Setup asks whether to keep
     your settings (the CONFIG folder); say Yes to upgrade or repair DVX and
    -keep them.
    +keep them. Setup starts with the fonts the folder already has; choose
    +others to add fonts or remove them.
     
     
     Removing DVX
    @@ -66,11 +77,12 @@ DVX /SAFE, to start it in 640x480.
     Unattended install
     ------------------
     
    -    SETUP /AUTO=C:\DVX [/LOG=C:\SETUP.LOG]
    +    SETUP /AUTO=C:\DVX [/FONTS=NONE|BASE|ALL] [/LOG=C:\SETUP.LOG]
     
     installs without asking (the licence is taken as accepted, settings are
    -kept on an upgrade, AUTOEXEC.BAT is not changed). The exit code is 0
    -when DVX was installed.
    +kept on an upgrade, and AUTOEXEC.BAT is not changed). /FONTS chooses the
    +fonts; without it Setup installs the fonts the folder already has, or
    +all of them in a new folder. The exit code is 0 when DVX was installed.
     
     
     DVX is free software under the MIT licence (see the licence in Setup).
    diff --git a/src/installer/mkDist.sh b/src/installer/mkDist.sh
    index b6df174f..9a5d713a 100755
    --- a/src/installer/mkDist.sh
    +++ b/src/installer/mkDist.sh
    @@ -26,7 +26,10 @@
     #
     # From a built bin/ (make all):
     #   dist/dvx.zip            the package (bin/ without the build machine's
    -#                           files: host tools, tests, SDK, map, logs)
    +#                           files: host tools, tests, SDK, map, logs), in
    +#                           tiers: DVX, then the base set of fonts (mkfont
    +#                           -l base), then the other fonts -- Setup installs
    +#                           the fonts of none, the first, or both
     #   dist/cd/SETUP.EXE       SETUP.EXE with the package appended
     #   dist/cd/README.TXT
     #   dist/DVX.ISO            ISO 9660 image of dist/cd
    @@ -54,6 +57,10 @@ BIN="${DVX_DIST_BIN:-${ROOT}/bin}"
     DIST="${DVX_DIST_OUT:-${ROOT}/dist}"
     STAGE="${DIST}/stage"
     TOOL="${BIN}/host/dvxpkg"
    +MKFONT="${BIN}/host/mkfont"
    +FONT_LIST="${ROOT}/src/fonts/fonts.ini"
    +# bin/'s fonts folder (DVX_FONTS_DIR, any case)
    +FONTS_DIR="fonts"
     SETUP="${BIN}/setup.exe"
     README="${ROOT}/src/installer/README.TXT"
     FLOPPY="${1:-1.44M}"
    @@ -87,7 +94,7 @@ format() {
     
     command -v zip > /dev/null || fail "zip is missing (it packs dist/sdk.zip)"
     
    -for f in "${TOOL}" "${SETUP}"; do
    +for f in "${TOOL}" "${SETUP}" "${MKFONT}"; do
         [ -x "${f}" ] || [ -f "${f}" ] || fail "${f} is missing (run make first)"
     done
     
    @@ -95,7 +102,15 @@ rm -rf "${DIST}"
     mkdir -p "${STAGE}" "${DIST}/cd" "${DIST}/floppy" "${DIST}/parts"
     
     tar -C "${BIN}" "${EXCLUDES[@]}" -cf - . | tar -C "${STAGE}" -xf -
    -"${TOOL}" zip "${STAGE}" "${DIST}/dvx.zip"
    +
    +# The tiers after DVX's own files: the base set of fonts (the folder and
    +# what mkfont lists), then every other font
    +TIER_BASE="${DIST}/tierbase.lst"
    +TIER_MORE="${DIST}/tiermore.lst"
    +{ echo "${FONTS_DIR}/"; "${MKFONT}" -l "${FONT_LIST}" base | sed "s|^|${FONTS_DIR}/|"; } > "${TIER_BASE}" || fail "mkfont could not list the base set of fonts"
    +echo "${FONTS_DIR}/*" > "${TIER_MORE}"
    +"${TOOL}" zip "${STAGE}" "${DIST}/dvx.zip" "${TIER_BASE}" "${TIER_MORE}"
    +rm -f "${TIER_BASE}" "${TIER_MORE}"
     sed 's/$/\r/' "${README}" > "${DIST}/README.TXT"
     
     SETUP_SIZE=$(stat -c %s "${SETUP}")
    diff --git a/src/installer/pkgBuild.c b/src/installer/pkgBuild.c
    index 88f9a90d..631c3934 100644
    --- a/src/installer/pkgBuild.c
    +++ b/src/installer/pkgBuild.c
    @@ -29,6 +29,7 @@
     #include 
     #include 
     #include 
    +#include 
     #include 
     
     #define PATH_MAX_LEN   1024
    @@ -42,24 +43,43 @@ typedef struct {
         int32_t  cap;
     } NameListT;
     
    +// A name and the tier the rules put it in, for the ZIP's order
    +typedef struct {
    +    const char *name;
    +    int32_t     tier;
    +} TieredNameT;
    +
     // ============================================================
     // Prototypes
     // ============================================================
     
    -static int     byName(const void *a, const void *b);
    +static int     byTierName(const void *a, const void *b);
    +static int32_t entryTier(const mz_zip_archive_file_stat *st);
     static bool    listAdd(NameListT *list, const char *name);
     static void    listFree(NameListT *list);
     static bool    loadFile(const char *path, uint8_t **data, uint32_t *size);
     bool           pkgBuildAppended(const char *exePath, const char *zipPath, const char *outPath, char *err, int32_t errSize);
     bool           pkgBuildInfo(const char *zipPath, PkgHeaderT *info, char *err, int32_t errSize);
     int32_t        pkgBuildSplit(const char *zipPath, const char *outDir, uint32_t firstRoom, uint32_t diskRoom, char *err, int32_t errSize);
    -bool           pkgBuildZip(const char *folder, const char *zipPath, char *err, int32_t errSize);
    +bool           pkgBuildZip(const char *folder, const char *zipPath, const PkgTierRuleT *rules, int32_t ruleCount, char *err, int32_t errSize);
     static bool    scanFolder(const char *root, const char *rel, NameListT *list, char *err, int32_t errSize);
    +static int32_t tierOf(const char *name, const PkgTierRuleT *rules, int32_t ruleCount);
     static bool    writePart(const char *path, const uint8_t *data, uint32_t len, const PkgHeaderT *h, const uint8_t *prefix, uint32_t prefixLen);
     
     
    -static int byName(const void *a, const void *b) {
    -    return strcmp(*(char *const *)a, *(char *const *)b);
    +static int byTierName(const void *a, const void *b) {
    +    const TieredNameT *x = (const TieredNameT *)a;
    +    const TieredNameT *y = (const TieredNameT *)b;
    +
    +    return (x->tier != y->tier) ? (int)(x->tier - y->tier) : strcmp(x->name, y->name);
    +}
    +
    +
    +// The tier a ZIP entry's comment gives it (none: tier 0)
    +static int32_t entryTier(const mz_zip_archive_file_stat *st) {
    +    char digit = st->m_comment[0];
    +
    +    return (st->m_comment_size == PKG_TIER_COMMENT_LEN && digit > '0' && digit < '0' + PKG_TIERS) ? digit - '0' : 0;
     }
     
     
    @@ -184,13 +204,58 @@ bool pkgBuildInfo(const char *zipPath, PkgHeaderT *info, char *err, int32_t errS
             return false;
         }
     
    -    for (mz_uint i = 0; i < mz_zip_reader_get_num_files(&zip); i++) {
    +    // Each tier's first entry (the end of the tiers before it), and the
    +    // files and bytes of every tier counted into its own and each later one
    +    uint64_t firstOf[PKG_TIERS];
    +    int32_t  lastTier = 0;
    +    bool     ordered  = true;
    +
    +    for (int32_t t = 0; t < PKG_TIERS; t++) {
    +        firstOf[t] = zip.m_central_directory_file_ofs;
    +    }
    +
    +    for (mz_uint i = 0; ordered && i < mz_zip_reader_get_num_files(&zip); i++) {
             mz_zip_archive_file_stat st;
     
    -        if (mz_zip_reader_file_stat(&zip, i, &st) && !st.m_is_directory) {
    -            info->fileCount++;
    -            info->unpackedBytes += st.m_uncomp_size;
    +        if (!mz_zip_reader_file_stat(&zip, i, &st)) {
    +            continue;
             }
    +
    +        int32_t tier = entryTier(&st);
    +
    +        if (tier < lastTier) {
    +            snprintf(err, errSize, "%s: %s comes after files of a later tier", zipPath, st.m_filename);
    +            ordered = false;
    +        }
    +
    +        lastTier = tier;
    +
    +        if (st.m_local_header_ofs < firstOf[tier]) {
    +            firstOf[tier] = st.m_local_header_ofs;
    +        }
    +
    +        for (int32_t t = tier; !st.m_is_directory && t < PKG_TIERS; t++) {
    +            info->tiers[t].fileCount++;
    +            info->tiers[t].bytes += st.m_uncomp_size;
    +        }
    +    }
    +
    +    for (int32_t t = 0; t < PKG_TIERS; t++) {
    +        uint64_t end = zip.m_central_directory_file_ofs;
    +
    +        for (int32_t later = t + 1; later < PKG_TIERS; later++) {
    +            if (firstOf[later] < end) {
    +                end = firstOf[later];
    +            }
    +        }
    +
    +        info->tiers[t].end = (uint32_t)end;
    +    }
    +
    +    if (!ordered) {
    +        mz_zip_reader_end(&zip);
    +        free(data);
    +        return false;
         }
     
         info->zipSize = size;
    @@ -257,7 +322,7 @@ int32_t pkgBuildSplit(const char *zipPath, const char *outDir, uint32_t firstRoo
     }
     
     
    -bool pkgBuildZip(const char *folder, const char *zipPath, char *err, int32_t errSize) {
    +bool pkgBuildZip(const char *folder, const char *zipPath, const PkgTierRuleT *rules, int32_t ruleCount, char *err, int32_t errSize) {
         NameListT      list;
         mz_zip_archive zip;
         bool           ok;
    @@ -270,12 +335,22 @@ bool pkgBuildZip(const char *folder, const char *zipPath, char *err, int32_t err
             return false;
         }
     
    -    // An empty folder leaves list.names NULL; qsort's base must not be
    -    // NULL even for a zero count.
    -    if (list.names) {
    -        qsort(list.names, (size_t)list.count, sizeof(char *), byName);
    +    // Every name with its tier, in the ZIP's order (an empty folder has
    +    // no names: calloc of none may return NULL)
    +    TieredNameT *order = (TieredNameT *)calloc((size_t)list.count + 1, sizeof(TieredNameT));
    +
    +    if (!order) {
    +        snprintf(err, errSize, "out of memory sorting %s", folder);
    +        listFree(&list);
    +        return false;
         }
     
    +    for (int32_t i = 0; i < list.count; i++) {
    +        order[i].name = list.names[i];
    +        order[i].tier = tierOf(list.names[i], rules, ruleCount);
    +    }
    +
    +    qsort(order, (size_t)list.count, sizeof(TieredNameT), byTierName);
         ok = mz_zip_writer_init_file(&zip, zipPath, 0);
     
         if (!ok) {
    @@ -283,16 +358,18 @@ bool pkgBuildZip(const char *folder, const char *zipPath, char *err, int32_t err
         }
     
         for (int32_t i = 0; ok && i < list.count; i++) {
    -        const char *name = list.names[i];
    -        size_t      len  = strlen(name);
    +        const char *name       = order[i].name;
    +        size_t      len        = strlen(name);
    +        char        comment    = (char)('0' + order[i].tier);
    +        mz_uint16   commentLen = order[i].tier ? PKG_TIER_COMMENT_LEN : 0;
     
             if (name[len - 1] == PKG_NAME_SEP) {
    -            ok = mz_zip_writer_add_mem(&zip, name, "", 0, MZ_NO_COMPRESSION);
    +            ok = mz_zip_writer_add_mem_ex(&zip, name, "", 0, &comment, commentLen, MZ_NO_COMPRESSION, 0, 0);
             } else {
                 char full[PATH_MAX_LEN];
     
                 snprintf(full, sizeof(full), "%s/%s", folder, name);
    -            ok = mz_zip_writer_add_file(&zip, name, full, NULL, 0, MZ_UBER_COMPRESSION | MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE | MZ_ZIP_FLAG_ASCII_FILENAME);
    +            ok = mz_zip_writer_add_file(&zip, name, full, &comment, commentLen, MZ_UBER_COMPRESSION | MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE | MZ_ZIP_FLAG_ASCII_FILENAME);
             }
     
             if (!ok) {
    @@ -306,6 +383,7 @@ bool pkgBuildZip(const char *folder, const char *zipPath, char *err, int32_t err
         }
     
         mz_zip_writer_end(&zip);
    +    free(order);
         listFree(&list);
     
         if (!ok) {
    @@ -367,6 +445,27 @@ static bool scanFolder(const char *root, const char *rel, NameListT *list, char
     }
     
     
    +// The tier the rules put name in (0: no rule takes it)
    +static int32_t tierOf(const char *name, const PkgTierRuleT *rules, int32_t ruleCount) {
    +    size_t wildLen = strlen(PKG_TIER_WILD);
    +
    +    for (int32_t r = 0; r < ruleCount; r++) {
    +        for (int32_t i = 0; i < rules[r].count; i++) {
    +            const char *rule = rules[r].names[i];
    +            size_t      len  = strlen(rule);
    +            bool        wild = len >= wildLen && strcmp(rule + len - wildLen, PKG_TIER_WILD) == 0;
    +
    +            // A wildcard rule's folder name keeps its '/': "FONTS/*" takes "FONTS/" and "FONTS/A.DFN"
    +            if (wild ? strncasecmp(name, rule, len - wildLen + 1) == 0 : strcasecmp(name, rule) == 0) {
    +                return r + 1;
    +            }
    +        }
    +    }
    +
    +    return 0;
    +}
    +
    +
     // A part: prefix (the program, for the CD), data, then the header.
     static bool writePart(const char *path, const uint8_t *data, uint32_t len, const PkgHeaderT *h, const uint8_t *prefix, uint32_t prefixLen) {
         uint8_t hdr[PKG_HEADER_SIZE];
    diff --git a/src/installer/pkgBuild.h b/src/installer/pkgBuild.h
    index 3e5e5be9..ee080be6 100644
    --- a/src/installer/pkgBuild.h
    +++ b/src/installer/pkgBuild.h
    @@ -34,12 +34,25 @@
     // Longest message the functions below return
     #define PKG_BUILD_ERR_MAX 400
     
    +// A tier rule ending in this takes every name under its folder
    +#define PKG_TIER_WILD     "/*"
    +
    +// The names of a later tier (pkgBuildZip): each a file or folder name as
    +// the ZIP holds it ('/' between parts, a folder's ending in '/'), or a
    +// folder's name followed by PKG_TIER_WILD, which takes the folder and
    +// everything under it.  Letter case is ignored.
    +typedef struct {
    +    const char *const *names;
    +    int32_t            count;
    +} PkgTierRuleT;
    +
     // The CD form: outPath = the program at exePath, the ZIP, and a header
     // (disk 1 of 1).
     bool    pkgBuildAppended(const char *exePath, const char *zipPath, const char *outPath, char *err, int32_t errSize);
     
    -// What a finished ZIP holds: set ID, file count and unpacked size (the
    -// part fields are left 0).
    +// What a finished ZIP holds: set ID, size, and each tier's end, files and
    +// unpacked size (the part fields are left 0).  False when a file comes
    +// after one of a later tier.
     bool    pkgBuildInfo(const char *zipPath, PkgHeaderT *info, char *err, int32_t errSize);
     
     // The floppy form: cut the ZIP into outDir/DVX.P01, DVX.P02, ... where
    @@ -48,8 +61,11 @@ bool    pkgBuildInfo(const char *zipPath, PkgHeaderT *info, char *err, int32_t e
     // Returns the number of disks, or -1 with a reason in err.
     int32_t pkgBuildSplit(const char *zipPath, const char *outDir, uint32_t firstRoom, uint32_t diskRoom, char *err, int32_t errSize);
     
    -// ZIP every file and folder under folder (sorted by name, best
    -// compression, sizes in the local headers).  Every name must be DOS 8.3.
    -bool    pkgBuildZip(const char *folder, const char *zipPath, char *err, int32_t errSize);
    +// ZIP every file and folder under folder (best compression, sizes in the
    +// local headers), tier by tier and by name in each: rules[t] holds the
    +// names of tier t + 1 (ruleCount at most PKG_TIERS - 1; a name in no rule
    +// is tier 0, one in several in the first one's tier).  Every name must be
    +// DOS 8.3.
    +bool    pkgBuildZip(const char *folder, const char *zipPath, const PkgTierRuleT *rules, int32_t ruleCount, char *err, int32_t errSize);
     
     #endif // PKG_BUILD_H
    diff --git a/src/installer/pkgFormat.c b/src/installer/pkgFormat.c
    index 2218f184..103a1b80 100644
    --- a/src/installer/pkgFormat.c
    +++ b/src/installer/pkgFormat.c
    @@ -36,10 +36,15 @@
     #define HDR_PART_OFFSET 20
     #define HDR_PART_LENGTH 24
     #define HDR_ZIP_SIZE    28
    -#define HDR_FILE_COUNT  32
    -#define HDR_UNPACKED    36
    -#define HDR_UNPACKED_HI 40
    -#define HDR_CRC         44
    +#define HDR_TIERS       32
    +#define HDR_TIER_SIZE   16
    +#define HDR_CRC         (HDR_TIERS + HDR_TIER_SIZE * PKG_TIERS)
    +
    +// A tier's fields, from its start
    +#define TIER_END        0
    +#define TIER_FILES      4
    +#define TIER_BYTES      8
    +#define TIER_BYTES_HI   12
     
     #define BYTE_BITS       8
     #define BYTE_MASK       0xFF
    @@ -127,10 +132,23 @@ bool pkgHeaderDecode(const uint8_t *in, PkgHeaderT *h) {
         h->partOffset    = pkgGet32(in + HDR_PART_OFFSET);
         h->partLength    = pkgGet32(in + HDR_PART_LENGTH);
         h->zipSize       = pkgGet32(in + HDR_ZIP_SIZE);
    -    h->fileCount     = pkgGet32(in + HDR_FILE_COUNT);
    -    h->unpackedBytes = (uint64_t)pkgGet32(in + HDR_UNPACKED) | ((uint64_t)pkgGet32(in + HDR_UNPACKED_HI) << WORD_BITS);
     
    -    return h->disk >= 1 && h->disk <= h->diskCount && h->diskCount <= PKG_MAX_DISKS && h->partOffset <= h->zipSize && h->partLength <= h->zipSize - h->partOffset;
    +    // Each tier ends where the one before it does or later, inside the ZIP
    +    bool     ok   = h->disk >= 1 && h->disk <= h->diskCount && h->diskCount <= PKG_MAX_DISKS && h->partOffset <= h->zipSize && h->partLength <= h->zipSize - h->partOffset;
    +    PkgTierT prev = { 0, 0, 0 };
    +
    +    for (int32_t t = 0; t < PKG_TIERS; t++) {
    +        const uint8_t *p    = in + HDR_TIERS + t * HDR_TIER_SIZE;
    +        PkgTierT      *tier = &h->tiers[t];
    +
    +        tier->end       = pkgGet32(p + TIER_END);
    +        tier->fileCount = pkgGet32(p + TIER_FILES);
    +        tier->bytes     = (uint64_t)pkgGet32(p + TIER_BYTES) | ((uint64_t)pkgGet32(p + TIER_BYTES_HI) << WORD_BITS);
    +        ok              = ok && tier->end >= prev.end && tier->end <= h->zipSize && tier->fileCount >= prev.fileCount && tier->bytes >= prev.bytes;
    +        prev            = *tier;
    +    }
    +
    +    return ok;
     }
     
     
    @@ -144,9 +162,17 @@ void pkgHeaderEncode(const PkgHeaderT *h, uint8_t *out) {
         put32(out + HDR_PART_OFFSET, h->partOffset);
         put32(out + HDR_PART_LENGTH, h->partLength);
         put32(out + HDR_ZIP_SIZE, h->zipSize);
    -    put32(out + HDR_FILE_COUNT, h->fileCount);
    -    put32(out + HDR_UNPACKED, (uint32_t)h->unpackedBytes);
    -    put32(out + HDR_UNPACKED_HI, (uint32_t)(h->unpackedBytes >> WORD_BITS));
    +
    +    for (int32_t t = 0; t < PKG_TIERS; t++) {
    +        uint8_t        *p    = out + HDR_TIERS + t * HDR_TIER_SIZE;
    +        const PkgTierT *tier = &h->tiers[t];
    +
    +        put32(p + TIER_END, tier->end);
    +        put32(p + TIER_FILES, tier->fileCount);
    +        put32(p + TIER_BYTES, (uint32_t)tier->bytes);
    +        put32(p + TIER_BYTES_HI, (uint32_t)(tier->bytes >> WORD_BITS));
    +    }
    +
         put32(out + HDR_CRC, (uint32_t)mz_crc32(MZ_CRC32_INIT, out, HDR_CRC));
     }
     
    diff --git a/src/installer/pkgFormat.h b/src/installer/pkgFormat.h
    index 88762962..6627b779 100644
    --- a/src/installer/pkgFormat.h
    +++ b/src/installer/pkgFormat.h
    @@ -30,8 +30,17 @@
     // set ID (the CRC-32 of the whole ZIP, the same in every part), the
     // part's disk number and the number of disks, so Setup asks for the right
     // disk and refuses a disk from another set.  Disk 1's header (or the CD's)
    -// also says how many files the ZIP holds and how many bytes they unpack
    -// to, for the free space check and the progress bar.
    +// also says where each tier of files ends in the ZIP, how many files it
    +// holds and how many bytes they unpack to, for the choice of what to
    +// install, the free space check, and the progress bar.
    +//
    +// Tiers: the files come in PKG_TIERS groups, in this order in the ZIP.
    +// Tier 0 is always installed; a later tier is installed with every tier
    +// before it (DVX: the program, the base set of fonts, the other fonts), so
    +// Setup stops reading at the end of the last tier chosen and never asks
    +// for the disks after it.  A file of a later tier carries its tier as its
    +// ZIP file comment, one digit (pkgBuildZip writes it, pkgBuildInfo reads
    +// it).
     //
     // The ZIP's local headers carry their sizes (no data descriptors), so
     // Setup reads the ZIP front to back as a stream, across disks, and checks
    @@ -50,10 +59,14 @@
     
     // Part header: magic, format version, then the fields of PkgHeaderT and
     // a CRC-32 of everything before it.  Little-endian.
    -#define PKG_HEADER_SIZE      48
    +#define PKG_HEADER_SIZE      84
     #define PKG_MAGIC            "DVXPKG\032"
     #define PKG_MAGIC_SIZE       8
    -#define PKG_FORMAT_VERSION   1
    +#define PKG_FORMAT_VERSION   2
    +
    +// Tiers of files (see above), and the length of a later tier's comment
    +#define PKG_TIERS            3
    +#define PKG_TIER_COMMENT_LEN 1
     
     // Part file names: DVX.P01 .. DVX.P99 (PKG_PART_NAME_MAX with the NUL)
     #define PKG_PART_FMT         "DVX.P%02d"
    @@ -80,6 +93,13 @@
     #define PKG_DOS_BASE_MAX     8
     #define PKG_DOS_EXT_MAX      3
     
    +// Tiers 0 .. n of the package together
    +typedef struct {
    +    uint32_t end;            // ZIP offset after their files: the next tier's first file, or the central directory
    +    uint32_t fileCount;      // their files (not folders)
    +    uint64_t bytes;          // their size unpacked
    +} PkgTierT;
    +
     typedef struct {
         uint16_t disk;           // this part's disk, 1 .. diskCount
         uint16_t diskCount;      // parts in the set
    @@ -87,8 +107,7 @@ typedef struct {
         uint32_t partOffset;     // where this part's bytes start in the ZIP
         uint32_t partLength;     // bytes of the ZIP in this part
         uint32_t zipSize;        // bytes in the whole ZIP
    -    uint32_t fileCount;      // files in the ZIP (not folders)
    -    uint64_t unpackedBytes;  // their size unpacked
    +    PkgTierT tiers[PKG_TIERS];  // tiers[n]: tiers 0 .. n together (tiers[PKG_TIERS - 1]: the whole package)
     } PkgHeaderT;
     
     // Parse a header; false when the magic, version, CRC or fields are wrong.
    diff --git a/src/installer/pkgRead.c b/src/installer/pkgRead.c
    index 594094d8..43d15a22 100644
    --- a/src/installer/pkgRead.c
    +++ b/src/installer/pkgRead.c
    @@ -74,6 +74,7 @@ typedef enum {
     
     struct PkgJobT {
         PkgSourceT          src;
    +    PkgTierT            want;               // the tiers to unpack together: where they end, files, bytes
         char                destDir[DVX_MAX_PATH];
         PkgAskDiskFnT       ask;
         PkgKeepFnT          keep;
    @@ -132,7 +133,7 @@ static PkgDiskProblemE otherDisk(const PkgJobT *job, int32_t wanted);
     static bool         partPath(const PkgSourceT *src, int32_t disk, char *out, int32_t outSize);
     void                pkgFind(const char *exePath, PkgSourceT *src);
     void                pkgJobCancel(PkgJobT *job);
    -PkgJobT            *pkgJobCreate(const PkgSourceT *src, const char *destDir, PkgAskDiskFnT ask, PkgKeepFnT keep, void *user);
    +PkgJobT            *pkgJobCreate(const PkgSourceT *src, const char *destDir, int32_t tier, PkgAskDiskFnT ask, PkgKeepFnT keep, void *user);
     char *const        *pkgJobCreatedDirs(const PkgJobT *job);
     char *const        *pkgJobFiles(const PkgJobT *job);
     void                pkgJobFree(PkgJobT *job);
    @@ -143,6 +144,7 @@ char *const        *pkgJobWritten(const PkgJobT *job);
     int32_t             pkgPartInDir(const char *dir);
     static bool         readBytes(PkgJobT *job, uint8_t *buf, uint32_t len);
     static bool         writeOut(PkgJobT *job, const uint8_t *data, uint32_t len);
    +static uint32_t     zipConsumed(const PkgJobT *job);
     
     
     static time_t dosTimeToTime(uint16_t date, uint16_t time) {
    @@ -302,19 +304,21 @@ static bool entryFinish(PkgJobT *job) {
     }
     
     
    -// The next local header (or the central directory: the end).
    +// The next local header, or the end: the end of the tiers to unpack (read
    +// no further, so no later disk is asked for) or the central directory.
     static PkgStepE entryHeader(PkgJobT *job) {
         uint8_t hdr[PKG_ZIP_LOCAL_SIZE];
    +    bool    end = zipConsumed(job) >= job->want.end;
     
    -    if (!readBytes(job, hdr, ZIP_SIG_SIZE)) {
    +    if (!end && !readBytes(job, hdr, ZIP_SIG_SIZE)) {
             return job->cancel ? PkgStepCancelledE : PkgStepErrorE;
         }
     
    -    uint32_t sig = pkgGet32(hdr);
    +    uint32_t sig = end ? PKG_ZIP_CENTRAL_SIG : pkgGet32(hdr);
     
         if (sig == PKG_ZIP_CENTRAL_SIG) {
    -        if (job->filesDone != (int32_t)job->src.info.fileCount) {
    -            return fail(job, "The DVX package is incomplete: %d of %d files.", (int)job->filesDone, (int)job->src.info.fileCount);
    +        if (job->filesDone != (int32_t)job->want.fileCount) {
    +            return fail(job, "The DVX package is incomplete: %d of %d files.", (int)job->filesDone, (int)job->want.fileCount);
             }
     
             job->state = JobDoneE;
    @@ -626,7 +630,7 @@ void pkgJobCancel(PkgJobT *job) {
     }
     
     
    -PkgJobT *pkgJobCreate(const PkgSourceT *src, const char *destDir, PkgAskDiskFnT ask, PkgKeepFnT keep, void *user) {
    +PkgJobT *pkgJobCreate(const PkgSourceT *src, const char *destDir, int32_t tier, PkgAskDiskFnT ask, PkgKeepFnT keep, void *user) {
         PkgJobT *job = (PkgJobT *)calloc(1, sizeof(PkgJobT));
     
         if (!job) {
    @@ -634,6 +638,7 @@ PkgJobT *pkgJobCreate(const PkgSourceT *src, const char *destDir, PkgAskDiskFnT
         }
     
         job->src   = *src;
    +    job->want  = src->info.tiers[DVX_MAX(0, DVX_MIN(tier, PKG_TIERS - 1))];
         job->ask   = ask;
         job->keep  = keep;
         job->user  = user;
    @@ -699,10 +704,10 @@ void pkgJobProgress(const PkgJobT *job, PkgProgressT *out) {
         uint64_t done = job->bytesDone + (job->state == JobDataE ? job->sizeDone : 0);
     
         memset(out, 0, sizeof(*out));
    -    out->bytesTotal = job->src.info.unpackedBytes;
    +    out->bytesTotal = job->want.bytes;
         out->bytesDone  = done > out->bytesTotal ? out->bytesTotal : done;
         out->filesDone  = job->filesDone;
    -    out->fileCount  = (int32_t)job->src.info.fileCount;
    +    out->fileCount  = (int32_t)job->want.fileCount;
         out->percent    = out->bytesTotal ? (int32_t)(out->bytesDone * PERCENT_FULL / out->bytesTotal) : PERCENT_FULL;
         out->disk       = job->disk;
         out->current    = job->state == JobDataE ? job->shownName : "";
    @@ -790,3 +795,10 @@ static bool writeOut(PkgJobT *job, const uint8_t *data, uint32_t len) {
         job->sizeDone += len;
         return true;
     }
    +
    +
    +// The ZIP offset of the next byte to be taken (read from the parts, less
    +// what is still buffered)
    +static uint32_t zipConsumed(const PkgJobT *job) {
    +    return job->zipPos - (job->inLen - job->inPos);
    +}
    diff --git a/src/installer/pkgRead.h b/src/installer/pkgRead.h
    index a48f5943..d44784a5 100644
    --- a/src/installer/pkgRead.h
    +++ b/src/installer/pkgRead.h
    @@ -101,10 +101,12 @@ void         pkgFind(const char *exePath, PkgSourceT *src);
     // returns PkgStepCancelledE.
     void         pkgJobCancel(PkgJobT *job);
     
    -// Start unpacking src into destDir (which must exist).  keep may be NULL
    -// (replace everything).  NULL when out of memory or the package cannot
    -// be opened (see pkgJobMessage of NULL: a fixed message).
    -PkgJobT     *pkgJobCreate(const PkgSourceT *src, const char *destDir, PkgAskDiskFnT ask, PkgKeepFnT keep, void *user);
    +// Start unpacking tiers 0 .. tier of src (pkgFormat.h; PKG_TIERS - 1: the
    +// whole package) into destDir (which must exist).  The job ends at that
    +// tier's end, so the disks after it are never asked for.  keep may be
    +// NULL (replace everything).  NULL when out of memory or the package
    +// cannot be opened (see pkgJobMessage of NULL: a fixed message).
    +PkgJobT     *pkgJobCreate(const PkgSourceT *src, const char *destDir, int32_t tier, PkgAskDiskFnT ask, PkgKeepFnT keep, void *user);
     
     // Folders the job made (full paths, in the order made; stb_ds array owned
     // by the job).
    diff --git a/src/installer/pkgTool.c b/src/installer/pkgTool.c
    index 1c6e2c47..9d9383a0 100644
    --- a/src/installer/pkgTool.c
    +++ b/src/installer/pkgTool.c
    @@ -22,7 +22,10 @@
     
     // pkgTool.c  -- dvxpkg, the package tool of `make dist` (build machine)
     //
    -//   dvxpkg zip   FOLDER OUT.ZIP                    ZIP a staged DVX folder
    +//   dvxpkg zip   FOLDER OUT.ZIP [TIER.LST ...]     ZIP a staged DVX folder;
    +//                                                  each list file names a
    +//                                                  later tier's files, one
    +//                                                  rule a line (pkgBuild.h)
     //   dvxpkg cd    SETUP.EXE PKG.ZIP OUT.EXE         SETUP.EXE with the package
     //   dvxpkg split PKG.ZIP OUTDIR DISK1ROOM ROOM     DVX.P01.. for floppies
     //   dvxpkg info  FILE                              a part's header
    @@ -34,7 +37,8 @@
     #include 
     #include 
     
    -#define ARGS_ZIP    4
    +#define ARGS_ZIP    4               // without tier lists
    +#define LINE_MAX_LEN 512
     #define ARGS_CD     5
     #define ARGS_SPLIT  6
     #define ARGS_INFO   3
    @@ -45,7 +49,10 @@
     // ============================================================
     
     static int     cmdInfo(const char *path);
    +static void    freeRules(PkgTierRuleT *rules, int32_t count);
     static bool    parseBytes(const char *text, uint32_t *out);
    +static void    printTiers(const PkgHeaderT *h);
    +static bool    readRules(const char *path, PkgTierRuleT *rule);
     static int     usage(void);
     int            main(int argc, char *argv[]);
     
    @@ -66,11 +73,23 @@ static int cmdInfo(const char *path) {
         }
     
         fclose(f);
    -    printf("disk %u of %u, set %08" PRIX32 ", part bytes %" PRIu32 " at %" PRIu32 " (file offset %" PRIu32 "), zip %" PRIu32 " bytes, %" PRIu32 " files, %" PRIu64 " bytes unpacked\n", (unsigned)h.disk, (unsigned)h.diskCount, h.setId, h.partLength, h.partOffset, start, h.zipSize, h.fileCount, h.unpackedBytes);
    +    printf("disk %u of %u, set %08" PRIX32 ", part bytes %" PRIu32 " at %" PRIu32 " (file offset %" PRIu32 "), zip %" PRIu32 " bytes\n", (unsigned)h.disk, (unsigned)h.diskCount, h.setId, h.partLength, h.partOffset, start, h.zipSize);
    +    printTiers(&h);
         return EXIT_SUCCESS;
     }
     
     
    +static void freeRules(PkgTierRuleT *rules, int32_t count) {
    +    for (int32_t r = 0; r < count; r++) {
    +        for (int32_t i = 0; i < rules[r].count; i++) {
    +            free((char *)rules[r].names[i]);
    +        }
    +
    +        free((char **)rules[r].names);
    +    }
    +}
    +
    +
     static bool parseBytes(const char *text, uint32_t *out) {
         char    *end = NULL;
         uint64_t v   = strtoull(text, &end, 10);
    @@ -80,26 +99,94 @@ static bool parseBytes(const char *text, uint32_t *out) {
     }
     
     
    +// Each tier with the ones before it: where it ends, files, bytes unpacked
    +static void printTiers(const PkgHeaderT *h) {
    +    for (int32_t t = 0; t < PKG_TIERS; t++) {
    +        printf("tiers 0-%d: %" PRIu32 " files, %" PRIu64 " bytes unpacked, end at %" PRIu32 "\n", (int)t, h->tiers[t].fileCount, h->tiers[t].bytes, h->tiers[t].end);
    +    }
    +}
    +
    +
    +// A tier list file: one rule a line (blank lines skipped, line ends cut)
    +static bool readRules(const char *path, PkgTierRuleT *rule) {
    +    char   line[LINE_MAX_LEN];
    +    char **names = NULL;
    +    FILE  *f     = fopen(path, "r");
    +
    +    rule->names = NULL;
    +    rule->count = 0;
    +
    +    if (!f) {
    +        fprintf(stderr, "dvxpkg: cannot read %s\n", path);
    +        return false;
    +    }
    +
    +    while (fgets(line, sizeof(line), f)) {
    +        line[strcspn(line, "\r\n")] = '\0';
    +
    +        if (!line[0]) {
    +            continue;
    +        }
    +
    +        char **grown = (char **)realloc(names, (size_t)(rule->count + 1) * sizeof(char *));
    +        char  *copy  = strdup(line);
    +
    +        if (!grown || !copy) {
    +            free(copy);
    +            names       = grown ? grown : names;
    +            rule->names = (const char *const *)names;
    +            fclose(f);
    +            fprintf(stderr, "dvxpkg: out of memory reading %s\n", path);
    +            return false;
    +        }
    +
    +        names                = grown;
    +        names[rule->count++] = copy;
    +    }
    +
    +    fclose(f);
    +    rule->names = (const char *const *)names;
    +    return true;
    +}
    +
    +
     static int usage(void) {
    -    fprintf(stderr, "usage: dvxpkg zip FOLDER OUT.ZIP\n       dvxpkg cd SETUP.EXE PKG.ZIP OUT.EXE\n       dvxpkg split PKG.ZIP OUTDIR DISK1ROOM ROOM\n       dvxpkg info FILE\n");
    +    fprintf(stderr, "usage: dvxpkg zip FOLDER OUT.ZIP [TIER.LST ...]\n       dvxpkg cd SETUP.EXE PKG.ZIP OUT.EXE\n       dvxpkg split PKG.ZIP OUTDIR DISK1ROOM ROOM\n       dvxpkg info FILE\n");
         return EXIT_USAGE;
     }
     
     
     int main(int argc, char *argv[]) {
    -    char     err[PKG_BUILD_ERR_MAX];
    -    uint32_t first = 0;
    -    uint32_t room  = 0;
    +    char     err[PKG_BUILD_ERR_MAX] = "";
    +    uint32_t first                  = 0;
    +    uint32_t room                   = 0;
     
    -    if (argc == ARGS_ZIP && strcmp(argv[1], "zip") == 0) {
    -        PkgHeaderT info;
    +    if (argc >= ARGS_ZIP && argc < ARGS_ZIP + PKG_TIERS && strcmp(argv[1], "zip") == 0) {
    +        PkgTierRuleT rules[PKG_TIERS - 1];
    +        PkgHeaderT   info;
    +        int32_t      ruleCount = argc - ARGS_ZIP;
    +        bool         ok        = true;
    +
    +        memset(rules, 0, sizeof(rules));
    +
    +        for (int32_t r = 0; ok && r < ruleCount; r++) {
    +            ok = readRules(argv[ARGS_ZIP + r], &rules[r]);
    +        }
    +
    +        ok = ok && pkgBuildZip(argv[2], argv[3], rules, ruleCount, err, sizeof(err)) && pkgBuildInfo(argv[3], &info, err, sizeof(err));
    +        freeRules(rules, ruleCount);
    +
    +        // A list that cannot be read has said so already
    +        if (!ok) {
    +            if (err[0]) {
    +                fprintf(stderr, "dvxpkg: %s\n", err);
    +            }
     
    -        if (!pkgBuildZip(argv[2], argv[3], err, sizeof(err)) || !pkgBuildInfo(argv[3], &info, err, sizeof(err))) {
    -            fprintf(stderr, "dvxpkg: %s\n", err);
                 return EXIT_FAILURE;
             }
     
    -        printf("%" PRIu32 " files, %" PRIu64 " bytes unpacked, %" PRIu32 " bytes packed, set %08" PRIX32 "\n", info.fileCount, info.unpackedBytes, info.zipSize, info.setId);
    +        printf("%" PRIu32 " bytes packed, set %08" PRIX32 "\n", info.zipSize, info.setId);
    +        printTiers(&info);
             return EXIT_SUCCESS;
         }
     
    diff --git a/src/installer/setup.c b/src/installer/setup.c
    index 28854ea1..6435c524 100644
    --- a/src/installer/setup.c
    +++ b/src/installer/setup.c
    @@ -22,26 +22,33 @@
     
     // setup.c  -- SETUP.EXE: installs DVX from its CD or floppies
     //
    -// One self-contained DOS program: the DVX core and the seven widgets it
    +// One self-contained DOS program: the DVX core and the eight widgets it
     // uses are linked in (no DVX folder needed), and it never writes to the
     // disk it runs from.  The package is appended to SETUP.EXE on a CD, or in
     // DVX.P01.. beside it on floppies (pkgRead.h).  Pages in one window:
    -// licence, folder (with Uninstall), progress, done.  Without a package
    -// (the SETUP.EXE installed in the DVX folder) it offers only Uninstall of
    -// its own folder.
    +// licence, folder (with the choice of fonts, and Uninstall), progress,
    +// done.  The fonts are the package's later tiers: none, the base set, or
    +// all of them are unpacked (the disks after the last one chosen are never
    +// asked for), and an upgrade to fewer fonts removes the others.  Without
    +// a package (the SETUP.EXE installed in the DVX folder) it offers only
    +// Uninstall of its own folder.
     //
     // The window sits on a splash backdrop drawn into the desktop's wallpaper
    -// buffer: a blue gradient with a large shadowed DVX title and tagline
    -// (drawTextScaled; no picture files on the disks).  While files are
    -// unpacked, billboards about what DVX does take turns under the progress
    -// bar, and the done page celebrates; both are a small widget class of
    -// Setup's own (board*).
    +// buffer: a blue gradient with a large shadowed DVX title (the system
    +// font, drawTextScaled) and a tagline (no picture files on the disks).
    +// While files are unpacked, billboards about what DVX does take turns
    +// under the progress bar, and the done page celebrates; both are a small
    +// widget class of Setup's own (board*).  The tagline and the billboards
    +// are drawn in fonts Setup carries (setupFonts.h, made from
    +// setupFonts.ini), added for it at start.
     //
     //   SETUP                      the wizard
     //   SETUP /AUTO=C:\DVX         install there without questions (licence
     //                              taken as accepted, settings kept on an
     //                              upgrade, AUTOEXEC.BAT untouched); exit
     //                              code 0 when installed
    +//   SETUP /AUTO=.. /FONTS=BASE  NONE, BASE, or ALL fonts (else what the
    +//                              folder has; all on a new install)
     //   SETUP /LOG=C:\SETUP.LOG    write the log there (else it stays in memory)
     //   SETUP /AUTO=.. /SHOTS=C:\X  test only (the DOS test harness; not in the
     //                              user docs): also show every page an
    @@ -61,6 +68,7 @@
     #include "dvxDlg.h"
     #include "dvxDraw.h"
     #include "dvxPlat.h"
    +#include "dvxText.h"
     #include "dvxVideo.h"
     #include "dvxWgtP.h"
     #include "box/box.h"
    @@ -68,10 +76,12 @@
     #include "checkbox/checkbox.h"
     #include "label/label.h"
     #include "progressBar/progress.h"
    +#include "radio/radio.h"
     #include "textInput/textInpt.h"
     #include "fileops/fileOps.h"
     #include "pkgRead.h"
     #include "setupCore.h"
    +#include "setupFonts.h"
     #include "setupLicense.h"
     #include "stb_ds_wrap.h"
     
    @@ -102,6 +112,7 @@
     #define SETUP_ARG_AUTO     "/AUTO="
     #define SETUP_ARG_LOG      "/LOG="
     #define SETUP_ARG_SHOTS    "/SHOTS="
    +#define SETUP_ARG_FONTS    "/FONTS="
     #define SETUP_BYTES_PER_MB (1024 * 1024)
     #define SETUP_EXIT_FAILED  1
     #define SETUP_SHOT_UPGRADE "SU-UPGR.BMP"
    @@ -112,22 +123,30 @@
     #define SETUP_BOARD_LINES  3
     #define SETUP_BOARD_PAD    10       // inside a billboard's edges
     #define SETUP_BOARD_GAP    6        // between its headline and its lines
    -#define SETUP_BOARD_SCALE  2        // a copy-page headline's size (drawTextScaled)
    -#define SETUP_DONE_SCALE   3        // the done page's headline size
     #define SETUP_PERCENT      100      // sparkle positions are percentages of the board
     #define SETUP_SPARKLE_CORE 3        // the square at a sparkle's centre, pixels across
     
     // The splash backdrop
     #define SPLASH_TITLE       "DVX"
     #define SPLASH_TAGLINE     "The DOS Visual eXecutive"
    -#define SPLASH_SUBLINE     "Windows, programs and games for your DOS computer"
    +#define SPLASH_SUBLINE     "Windows, programs, and games for your DOS computer"
     #define SPLASH_MARGIN      24       // the title's distance from the screen's top-left corner
     #define SPLASH_TITLE_SCALE 6
    -#define SPLASH_TAG_SCALE   2
     #define SPLASH_SHADOW      4        // the title's shadow offset, pixels
    -#define SPLASH_TAG_DROP    20       // the tagline's top below the title's
    +#define SPLASH_TAG_DROP    24       // the tagline's top below the title's
     #define SPLASH_SUB_GAP     12       // between the tagline and the line under it
     
    +// The fonts Setup carries (setupFonts.ini): their family, and per use
    +// its size and the shadow under it
    +#define SETUP_FONT_FAMILY   "DejaVu Sans"
    +#define SETUP_HEAD_SIZE     24      // the tagline and a billboard's headline (bold)
    +#define SETUP_HEAD_SHADOW   2
    +#define SETUP_BANNER_SIZE   36      // the done page's headline (bold)
    +#define SETUP_BANNER_SHADOW 3
    +#define SETUP_TEXT_SIZE     14      // the lines under them
    +#define SETUP_UNSCALED      1       // drawShadowed's scale for a font drawn at its size
    +#define SETUP_FONT_OWNER    0       // Setup's fonts are added for every program (Setup is the only one)
    +
     // The widgets linked in (each module's wgtRegister, renamed by the
     // Makefile)
     void wgtRegisterBox(void);
    @@ -135,6 +154,7 @@ void wgtRegisterButton(void);
     void wgtRegisterCheckbox(void);
     void wgtRegisterLabel(void);
     void wgtRegisterProgress(void);
    +void wgtRegisterRadio(void);
     void wgtRegisterSpacer(void);
     void wgtRegisterTextInput(void);
     
    @@ -154,11 +174,26 @@ typedef struct {
         uint8_t b;
     } SetupRgbT;
     
    -// One billboard: a headline (scale times the font's size) and lines
    +// What each of Setup's fonts draws
    +typedef enum {
    +    SetupTypeHeadE,     // the tagline and a billboard's headline
    +    SetupTypeBannerE,   // the done page's headline
    +    SetupTypeTextE,     // the lines under them
    +    SetupTypeCountE
    +} SetupTypeE;
    +
    +// A use's font (of SETUP_FONT_FAMILY) and the shadow under it
    +typedef struct {
    +    int32_t  size;
    +    uint32_t style;
    +    int32_t  shadow;    // offset, pixels (0: none)
    +} SetupTypeT;
    +
    +// One billboard: a headline (in its type's font) and lines
     typedef struct {
         const char *headline;
         const char *lines[SETUP_BOARD_LINES];
    -    int32_t     scale;
    +    SetupTypeE  type;
         bool        sparkles;
     } SetupBoardT;
     
    @@ -178,12 +213,15 @@ typedef struct {
     
     typedef struct {
         AppContextT     ctx;
    +    BitmapFontT    *fonts[SetupTypeCountE]; // per use (NULL: the system font)
         WindowT        *win;
         WidgetT        *pages[PageCountE];
         PageE           page;               // the one shown
         WidgetT        *accept;             // licence accepted
         WidgetT        *nextBtn;
    +    WidgetT        *spaceText;          // the room the choice of fonts needs
         WidgetT        *dest;               // target folder
    +    WidgetT        *fontsGroup;         // the fonts choice: a radio button per SetupFontsE
         WidgetT        *autoexec;           // "start with the computer" (NULL without a boot drive)
         WidgetT        *status;
         WidgetT        *bar;
    @@ -200,6 +238,9 @@ typedef struct {
         char            autoDest[DVX_MAX_PATH];
         char            logPath[DVX_MAX_PATH];
         char            shotDir[DVX_MAX_PATH];  // /SHOTS folder
    +    SetupFontsE     fontsArg;           // /FONTS=
    +    bool            fontsGiven;
    +    bool            fontsBad;           // /FONTS= is none of sFontsArgs
         bool            quit;
         bool            autoMode;
         bool            shots;              // /SHOTS with /AUTO: save every page
    @@ -224,6 +265,11 @@ static SetupT sSetup;
     // PkgFoundE for the log
     static const char *const sFoundNames[] = { "appended", "in parts", "none", "damaged" };
     
    +// The fonts choices (SetupFontsE order): /FONTS= (and the log), and the
    +// radio buttons (with the size each adds to DVX's own, but for none)
    +static const char *const sFontsArgs[SetupFontsCountE]   = { "NONE", "BASE", "ALL" };
    +static const char *const sFontsLabels[SetupFontsCountE] = { "&None: DVX's own font only", "Base s&et: DejaVu Sans, Sans Mono, and Serif", "A&ll fonts" };
    +
     // /SHOTS: each page's picture (PageE order, 8.3 names)
     static const char *const sShotNames[PageCountE] = { "SU-LIC.BMP", "SU-FOLD.BMP", "SU-COPY.BMP", "SU-DONE.BMP", "SU-UNIN.BMP" };
     
    @@ -242,15 +288,22 @@ static const SetupRgbT sShadow       = { 0, 0, 0 };
     
     // What DVX does, one billboard at a time while files are unpacked
     static const SetupBoardT sCopyBoards[] = {
    -    { "Windows, your way",       { "Move, size, stack and tile windows with the mouse or keys.", "Program Manager starts everything; Alt+Tab switches.", "Themes and wallpaper make the desktop your own." }, SETUP_BOARD_SCALE, false },
    -    { "Build your own programs", { "DVX BASIC: draw a form, add controls, write some code.", "Press F5 to run it, then make it a program of its own.", "Forms, graphics, files, sound and databases built in." }, SETUP_BOARD_SCALE, false },
    -    { "Games, sound and music",  { "Sound effects and music while you work or play.", "Joysticks and game pads, from BASIC and from C.", "Full-screen mode gives a game the whole display." }, SETUP_BOARD_SCALE, false },
    -    { "Help on every screen",    { "Press F1 anywhere for help about what you see.", "The Help Viewer holds every guide, and searches them.", "" }, SETUP_BOARD_SCALE, false },
    -    { "Files and the world",     { "File Manager copies, moves and opens your files.", "Terminal, networking and file transfer connect you.", "" }, SETUP_BOARD_SCALE, false }
    +    { "Windows, your way",       { "Move, size, stack, and tile windows with the mouse or keys.", "Program Manager starts everything; Alt+Tab switches.", "Themes and wallpaper make the desktop your own." }, SetupTypeHeadE, false },
    +    { "Build your own programs", { "DVX BASIC: draw a form, add controls, write some code.", "Press F5 to run it, then make it a program of its own.", "Forms, graphics, files, sound, and databases built in." }, SetupTypeHeadE, false },
    +    { "Games, sound, and music",  { "Sound effects and music while you work or play.", "Joysticks and game pads, from BASIC and from C.", "Full-screen mode gives a game the whole display." }, SetupTypeHeadE, false },
    +    { "Help on every screen",    { "Press F1 anywhere for help about what you see.", "The Help Viewer holds every guide, and searches them.", "" }, SetupTypeHeadE, false },
    +    { "Files and the world",     { "File Manager copies, moves, and opens your files.", "Terminal, networking, and file transfer connect you.", "" }, SetupTypeHeadE, false }
     };
     
     // The done page's celebration
    -static const SetupBoardT sDoneBoard = { "DVX is ready!", { "Your new desktop is waiting.", "", "" }, SETUP_DONE_SCALE, true };
    +static const SetupBoardT sDoneBoard = { "DVX is ready!", { "Your new desktop is waiting.", "", "" }, SetupTypeBannerE, true };
    +
    +// Setup's fonts, by use
    +static const SetupTypeT sTypes[SetupTypeCountE] = {
    +    [SetupTypeHeadE]   = { SETUP_HEAD_SIZE,   FONT_STYLE_BOLD, SETUP_HEAD_SHADOW   },
    +    [SetupTypeBannerE] = { SETUP_BANNER_SIZE, FONT_STYLE_BOLD, SETUP_BANNER_SHADOW },
    +    [SetupTypeTextE]   = { SETUP_TEXT_SIZE,   0,               0                   }
    +};
     
     // The sparkles take these colours in turn
     static const SetupRgbT *const sSparkleColors[] = { &sGold, &sWhite };
    @@ -270,44 +323,50 @@ static int32_t sBoardType = -1;
     // Prototypes
     // ============================================================
     
    -static int32_t  ask(const char *msg, int32_t flags, const char *shot, int32_t disk);
    -static bool     askDisk(void *user, int32_t disk, PkgDiskProblemE problem);
    -static void     boardCalcMinSize(WidgetT *w, const BitmapFontT *font);
    -static WidgetT *boardCreate(WidgetT *parent, const SetupBoardT *boards, int32_t count);
    -static void     boardDestroy(WidgetT *w);
    -static void     boardNext(WidgetT *w);
    -static void     boardPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
    -static void     buildPages(WidgetT *root);
    -static WidgetT *buttonRow(WidgetT *page);
    -static void     drawShadowed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t scale, int32_t offset, const SetupRgbT *color);
    -static void     drawSparkle(DisplayT *d, const BlitOpsT *ops, int32_t cx, int32_t cy, int32_t radius, uint32_t color);
    -static void     fillGradient(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const SetupRgbT *top, const SetupRgbT *bottom);
    -static void     installAutoexec(const char *destDir);
    -static bool     installCheck(const char *destDir, bool *upgrade, bool *keepSettings);
    -static PkgStepE installCopy(const char *destDir, bool keepSettings, bool newInstall);
    -static void     installRun(void);
    -static bool     keepSetting(void *user, const char *relName);
    -static void     onAccept(WidgetT *w);
    -static void     onAskIdle(void *ctx);
    -static void     onBack(WidgetT *w);
    -static void     onCancel(WidgetT *w);
    -static void     onClose(WindowT *win);
    -static void     onFinish(WidgetT *w);
    -static void     onInstall(WidgetT *w);
    -static void     onNext(WidgetT *w);
    -static void     onUninstall(WidgetT *w);
    -static void     onUninstallAsk(WidgetT *w);
    -static void     paintBackdrop(void);
    -static void     parseArgs(int argc, char *argv[]);
    -static void     report(const char *msg);
    -static void     shootFile(const char *name);
    -static void     shootPage(void);
    -static void     showPage(PageE page);
    -static void     showProgress(const PkgJobT *job);
    -static void     showUninstall(void);
    -static void     startDvx(void);
    -static void     uninstallRun(void);
    -int             main(int argc, char *argv[]);
    +static int32_t            ask(const char *msg, int32_t flags, const char *shot, int32_t disk);
    +static bool               askDisk(void *user, int32_t disk, PkgDiskProblemE problem);
    +static void               boardCalcMinSize(WidgetT *w, const BitmapFontT *font);
    +static WidgetT           *boardCreate(WidgetT *parent, const SetupBoardT *boards, int32_t count);
    +static void               boardDestroy(WidgetT *w);
    +static void               boardNext(WidgetT *w);
    +static void               boardPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
    +static void               buildPages(WidgetT *root);
    +static WidgetT           *buttonRow(WidgetT *page);
    +static void               drawShadowed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t scale, int32_t offset, const SetupRgbT *color);
    +static void               drawSparkle(DisplayT *d, const BlitOpsT *ops, int32_t cx, int32_t cy, int32_t radius, uint32_t color);
    +static void               fillGradient(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const SetupRgbT *top, const SetupRgbT *bottom);
    +static SetupFontsE        fontsChosen(const char *destDir);
    +static void               fontsLoad(void);
    +static void               installAutoexec(const char *destDir);
    +static bool               installCheck(const char *destDir, SetupFontsE fonts, bool *upgrade, bool *keepSettings);
    +static PkgStepE           installCopy(const char *destDir, SetupFontsE fonts, bool keepSettings, bool newInstall);
    +static void               installRun(void);
    +static bool               keepSetting(void *user, const char *relName);
    +static uint64_t           megabytes(uint64_t bytes);
    +static void               onAccept(WidgetT *w);
    +static void               onAskIdle(void *ctx);
    +static void               onBack(WidgetT *w);
    +static void               onCancel(WidgetT *w);
    +static void               onClose(WindowT *win);
    +static void               onFinish(WidgetT *w);
    +static void               onFonts(WidgetT *w);
    +static void               onInstall(WidgetT *w);
    +static void               onNext(WidgetT *w);
    +static void               onUninstall(WidgetT *w);
    +static void               onUninstallAsk(WidgetT *w);
    +static void               paintBackdrop(void);
    +static void               parseArgs(int argc, char *argv[]);
    +static void               report(const char *msg);
    +static void               shootFile(const char *name);
    +static void               shootPage(void);
    +static void               showPage(PageE page);
    +static void               showProgress(const PkgJobT *job);
    +static void               showSpace(void);
    +static void               showUninstall(void);
    +static void               startDvx(void);
    +static const BitmapFontT *typeFont(SetupTypeE type);
    +static void               uninstallRun(void);
    +int                       main(int argc, char *argv[]);
     
     // A billboard: paints itself, takes no input
     static const WidgetClassT sClassBoard = {
    @@ -385,23 +444,26 @@ static bool askDisk(void *user, int32_t disk, PkgDiskProblemE problem) {
     // Wide enough for the widest headline or line of any of its boards, tall
     // enough for the tallest board.
     static void boardCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    -    const BoardDataT *b = (const BoardDataT *)w->data;
    +    const BoardDataT  *b    = (const BoardDataT *)w->data;
    +    const BitmapFontT *text = typeFont(SetupTypeTextE);
     
    +    (void)font;
         w->calcMinW = 0;
         w->calcMinH = 0;
     
         for (int32_t i = 0; i < b->count; i++) {
             const SetupBoardT *board = &b->boards[i];
    -        int32_t            textW = textWidth(font, board->headline) * board->scale;
    +        const BitmapFontT *head  = typeFont(board->type);
    +        int32_t            textW = textWidth(head, board->headline);
             int32_t            rows  = 0;
     
             for (int32_t l = 0; l < SETUP_BOARD_LINES; l++) {
    -            textW = DVX_MAX(textW, textWidth(font, board->lines[l]));
    +            textW = DVX_MAX(textW, textWidth(text, board->lines[l]));
                 rows += board->lines[l][0] ? 1 : 0;
             }
     
             w->calcMinW = DVX_MAX(w->calcMinW, textW + SETUP_BOARD_PAD * 2);
    -        w->calcMinH = DVX_MAX(w->calcMinH, SETUP_BOARD_PAD * 2 + font->charHeight * board->scale + SETUP_BOARD_GAP + rows * font->charHeight);
    +        w->calcMinH = DVX_MAX(w->calcMinH, SETUP_BOARD_PAD * 2 + head->charHeight + SETUP_BOARD_GAP + rows * text->charHeight);
         }
     }
     
    @@ -446,20 +508,23 @@ static void boardNext(WidgetT *w) {
     // The board shown: a gradient, the shadowed headline, its lines and, on
     // the done page, sparkles.
     static void boardPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
    +    (void)font;
         (void)colors;
         const BoardDataT  *b     = (const BoardDataT *)w->data;
         const SetupBoardT *board = &b->boards[b->current];
    +    const BitmapFontT *head  = typeFont(board->type);
    +    const BitmapFontT *text  = typeFont(SetupTypeTextE);
         int32_t            x     = w->x + SETUP_BOARD_PAD;
         int32_t            y     = w->y + SETUP_BOARD_PAD;
         uint32_t           pale  = packColor(d, sPale.r, sPale.g, sPale.b);
     
         fillGradient(d, ops, w->x, w->y, w->w, w->h, &sBoardTop, &sBoardBottom);
    -    drawShadowed(d, ops, font, x, y, board->headline, board->scale, board->scale, board->sparkles ? &sGold : &sWhite);
    -    y += font->charHeight * board->scale + SETUP_BOARD_GAP;
    +    drawShadowed(d, ops, head, x, y, board->headline, SETUP_UNSCALED, sTypes[board->type].shadow, board->sparkles ? &sGold : &sWhite);
    +    y += head->charHeight + SETUP_BOARD_GAP;
     
         for (int32_t l = 0; l < SETUP_BOARD_LINES && board->lines[l][0]; l++) {
    -        drawText(d, ops, font, x, y, board->lines[l], pale, pale, false);
    -        y += font->charHeight;
    +        drawText(d, ops, text, x, y, board->lines[l], pale, pale, false);
    +        y += text->charHeight;
         }
     
         for (int32_t i = 0; board->sparkles && i < (int32_t)DVX_ARRAY_LEN(sSparkles); i++) {
    @@ -504,9 +569,8 @@ static void buildPages(WidgetT *root) {
         wgtSetEnabled(sSetup.nextBtn, false);
     
         // Folder
    -    p = sSetup.pages[PageFolderE];
    -    snprintf(text, sizeof(text), "DVX needs about %" PRIu64 " MB of disk space.", (setupSpaceNeeded(&sSetup.src.info) + SETUP_BYTES_PER_MB - 1) / SETUP_BYTES_PER_MB);
    -    wgtLabel(p, text);
    +    p                   = sSetup.pages[PageFolderE];
    +    sSetup.spaceText    = wgtLabel(p, "");
         wgtLabel(p, "Install DVX in this &folder:");
         sSetup.dest         = wgtTextInput(p, DVX_MAX_PATH - 1);
         sSetup.dest->weight = 0;             // one line high, not the page's spare room
    @@ -518,6 +582,28 @@ static void buildPages(WidgetT *root) {
         }
     
         wgtSetText(sSetup.dest, text);
    +
    +    // The fonts: to start with what the folder has (all on a new install),
    +    // or unattended what will be installed
    +    const PkgHeaderT *info = &sSetup.src.info;
    +
    +    wgtLabel(p, "Fonts to install:");
    +    sSetup.fontsGroup          = wgtRadioGroup(p);
    +    sSetup.fontsGroup->padding = wgtPixels(0);
    +
    +    for (int32_t f = 0; f < SetupFontsCountE; f++) {
    +        if (f == SetupFontsNoneE) {
    +            snprintf(text, sizeof(text), "%s", sFontsLabels[f]);
    +        } else {
    +            snprintf(text, sizeof(text), "%s (%" PRIu64 " MB)", sFontsLabels[f], megabytes(info->tiers[f].bytes - info->tiers[SetupFontsNoneE].bytes));
    +        }
    +
    +        wgtRadio(sSetup.fontsGroup, text);
    +    }
    +
    +    wgtRadioGroupSetSelected(sSetup.fontsGroup, (int32_t)(sSetup.autoMode ? fontsChosen(sSetup.autoDest) : setupFontsInstalled(wgtGetText(sSetup.dest), info)));
    +    sSetup.fontsGroup->onChange = onFonts;
    +    showSpace();
         // Labels do not wrap: two short statements, not one long sentence.
         wgtLabel(p, "Install also upgrades or repairs DVX in that folder.");
         wgtLabel(p, "Uninstall... removes DVX from that folder.");
    @@ -602,6 +688,30 @@ static void fillGradient(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y,
     }
     
     
    +// The fonts to install in destDir: /FONTS= or (unattended) what the
    +// folder has, else the choice on the folder page.
    +static SetupFontsE fontsChosen(const char *destDir) {
    +    if (sSetup.autoMode) {
    +        return sSetup.fontsGiven ? sSetup.fontsArg : setupFontsInstalled(destDir, &sSetup.src.info);
    +    }
    +
    +    return (SetupFontsE)wgtRadioGroupGetSelected(sSetup.fontsGroup);
    +}
    +
    +
    +// The fonts Setup carries (setupFonts.h) added, and each use's font
    +// opened (one that does not open stays NULL: the system font draws it)
    +static void fontsLoad(void) {
    +    for (int32_t i = 0; i < (int32_t)DVX_ARRAY_LEN(sSetupFonts); i++) {
    +        dvxFontAddMemory(SETUP_FONT_OWNER, sSetupFonts[i].data, sSetupFonts[i].length, sSetupFonts[i].name);
    +    }
    +
    +    for (int32_t t = 0; t < SetupTypeCountE; t++) {
    +        sSetup.fonts[t] = dvxFontOpen(SETUP_FONT_FAMILY, sTypes[t].size, sTypes[t].style);
    +    }
    +}
    +
    +
     // Show the exact line, ask, then back up AUTOEXEC.BAT and add it.
     static void installAutoexec(const char *destDir) {
         char path[DVX_MAX_PATH];
    @@ -634,11 +744,12 @@ static void installAutoexec(const char *destDir) {
     }
     
     
    -// The folder, the free space, and on a folder that already has DVX the
    -// choice between keeping and replacing the settings.
    -static bool installCheck(const char *destDir, bool *upgrade, bool *keepSettings) {
    +// The folder, the free space (on an upgrade, for more fonts than it has),
    +// and on a folder that already has DVX the choice between keeping and
    +// replacing the settings.
    +static bool installCheck(const char *destDir, SetupFontsE fonts, bool *upgrade, bool *keepSettings) {
         char     err[SETUP_ERR_MAX];
    -    uint64_t needed = setupSpaceNeeded(&sSetup.src.info);
    +    uint64_t needed = setupSpaceNeeded(&sSetup.src.info, fonts);
         uint64_t total  = 0;
         uint64_t avail  = 0;
     
    @@ -664,8 +775,15 @@ static bool installCheck(const char *destDir, bool *upgrade, bool *keepSettings)
             *keepSettings = answer == ID_YES;
         }
     
    -    // An upgrade replaces files of about the same size.
    -    if (!*upgrade && platformDiskSpace(destDir, &total, &avail) && avail < needed) {
    +    // An upgrade replaces files of about the same size; only more fonts
    +    // than the folder has need room.
    +    if (*upgrade) {
    +        uint64_t have = setupSpaceNeeded(&sSetup.src.info, setupFontsInstalled(destDir, &sSetup.src.info));
    +
    +        needed = (needed > have) ? needed - have : 0;
    +    }
    +
    +    if (needed > 0 && platformDiskSpace(destDir, &total, &avail) && avail < needed) {
             char msg[SETUP_MSG_MAX];
             char needText[FILEOPS_SIZE_TEXT_MAX];
             char freeText[FILEOPS_SIZE_TEXT_MAX];
    @@ -683,8 +801,8 @@ static bool installCheck(const char *destDir, bool *upgrade, bool *keepSettings)
     
     // Unpack in slices, drawing progress and seeing Cancel between them.
     // Returns how it ended; a new install that did not finish is removed.
    -static PkgStepE installCopy(const char *destDir, bool keepSettings, bool newInstall) {
    -    PkgJobT       *job        = pkgJobCreate(&sSetup.src, destDir, askDisk, keepSettings ? keepSetting : NULL, NULL);
    +static PkgStepE installCopy(const char *destDir, SetupFontsE fonts, bool keepSettings, bool newInstall) {
    +    PkgJobT       *job        = pkgJobCreate(&sSetup.src, destDir, (int32_t)fonts, askDisk, keepSettings ? keepSetting : NULL, NULL);
         PlatformTicksT slice      = PLATFORM_MS_TO_TICKS(SETUP_SLICE_MS);
         PlatformTicksT sliceStart = platformClock();
         PlatformTicksT boardTime  = PLATFORM_MS_TO_TICKS(SETUP_BOARD_MS);
    @@ -725,6 +843,8 @@ static PkgStepE installCopy(const char *destDir, bool keepSettings, bool newInst
         if (step == PkgStepDoneE) {
             char *const *files = pkgJobFiles(job);
     
    +        setupRemoveDropped(destDir, SETUP_FONTS_FOLDER, files, (int32_t)arrlen(files));
    +
             if (!setupListWrite(destDir, files, (int32_t)arrlen(files))) {
                 dvxLog("Setup: could not write %s in %s", SETUP_LIST_NAME, destDir);
             }
    @@ -762,7 +882,9 @@ static void installRun(void) {
         dvxCopyTrimmed(destDir, sizeof(destDir), sSetup.autoMode ? sSetup.autoDest : wgtGetText(sSetup.dest));
         platformPathNative(destDir);
     
    -    if (!installCheck(destDir, &upgrade, &keepSettings)) {
    +    SetupFontsE fonts = fontsChosen(destDir);
    +
    +    if (!installCheck(destDir, fonts, &upgrade, &keepSettings)) {
             sSetup.exitCode = SETUP_EXIT_FAILED;
             return;
         }
    @@ -779,9 +901,9 @@ static void installRun(void) {
         snprintf(sSetup.destDir, sizeof(sSetup.destDir), "%s", destDir);
         sSetup.cancelRequested = false;
         showPage(PageCopyE);
    -    dvxLog("Setup: installing in %s (%s)", destDir, upgrade ? (keepSettings ? "upgrade, settings kept" : "settings replaced") : "new");
    +    dvxLog("Setup: installing in %s (%s), fonts %s", destDir, upgrade ? (keepSettings ? "upgrade, settings kept" : "settings replaced") : "new", sFontsArgs[fonts]);
     
    -    PkgStepE result = installCopy(destDir, keepSettings, !upgrade);
    +    PkgStepE result = installCopy(destDir, fonts, keepSettings, !upgrade);
     
         if (result != PkgStepDoneE) {
             if (madeDest) {
    @@ -840,6 +962,12 @@ static bool keepSetting(void *user, const char *relName) {
     }
     
     
    +// bytes in megabytes, rounded up
    +static uint64_t megabytes(uint64_t bytes) {
    +    return (bytes + SETUP_BYTES_PER_MB - 1) / SETUP_BYTES_PER_MB;
    +}
    +
    +
     static void onAccept(WidgetT *w) {
         wgtSetEnabled(sSetup.nextBtn, wgtCheckboxIsChecked(w));
     }
    @@ -918,6 +1046,12 @@ static void onFinish(WidgetT *w) {
     }
     
     
    +static void onFonts(WidgetT *w) {
    +    (void)w;
    +    showSpace();
    +}
    +
    +
     static void onInstall(WidgetT *w) {
         (void)w;
         sSetup.installRequested = true;
    @@ -975,11 +1109,12 @@ static void paintBackdrop(void) {
         fillGradient(&d, &ctx->blitOps, 0, 0, d.width, d.height, &sSplashTop, &sSplashBottom);
         drawShadowed(&d, &ctx->blitOps, &ctx->font, SPLASH_MARGIN, SPLASH_MARGIN, title, SPLASH_TITLE_SCALE, SPLASH_SHADOW, &sGold);
     
    -    int32_t tagX = SPLASH_MARGIN * 2 + textWidth(&ctx->font, title) * SPLASH_TITLE_SCALE;
    -    int32_t tagY = SPLASH_MARGIN + SPLASH_TAG_DROP;
    +    const BitmapFontT *head = typeFont(SetupTypeHeadE);
    +    int32_t            tagX = SPLASH_MARGIN * 2 + textWidth(&ctx->font, title) * SPLASH_TITLE_SCALE;
    +    int32_t            tagY = SPLASH_MARGIN + SPLASH_TAG_DROP;
     
    -    drawShadowed(&d, &ctx->blitOps, &ctx->font, tagX, tagY, SPLASH_TAGLINE, SPLASH_TAG_SCALE, SPLASH_TAG_SCALE, &sWhite);
    -    drawText(&d, &ctx->blitOps, &ctx->font, tagX, tagY + ctx->font.charHeight * SPLASH_TAG_SCALE + SPLASH_SUB_GAP, SPLASH_SUBLINE, packColor(&d, sPale.r, sPale.g, sPale.b), 0, false);
    +    drawShadowed(&d, &ctx->blitOps, head, tagX, tagY, SPLASH_TAGLINE, SETUP_UNSCALED, sTypes[SetupTypeHeadE].shadow, &sWhite);
    +    drawText(&d, &ctx->blitOps, typeFont(SetupTypeTextE), tagX, tagY + head->charHeight + SPLASH_SUB_GAP, SPLASH_SUBLINE, packColor(&d, sPale.r, sPale.g, sPale.b), 0, false);
         free(ctx->wallpaperBuf);
         ctx->wallpaperBuf   = buf;
         ctx->wallpaperPitch = d.pitch;
    @@ -995,6 +1130,18 @@ static void parseArgs(int argc, char *argv[]) {
                 snprintf(sSetup.logPath, sizeof(sSetup.logPath), "%s", argv[i] + strlen(SETUP_ARG_LOG));
             } else if (strncasecmp(argv[i], SETUP_ARG_SHOTS, strlen(SETUP_ARG_SHOTS)) == 0) {
                 snprintf(sSetup.shotDir, sizeof(sSetup.shotDir), "%s", argv[i] + strlen(SETUP_ARG_SHOTS));
    +        } else if (strncasecmp(argv[i], SETUP_ARG_FONTS, strlen(SETUP_ARG_FONTS)) == 0) {
    +            const char *value = argv[i] + strlen(SETUP_ARG_FONTS);
    +
    +            sSetup.fontsBad = true;
    +
    +            for (int32_t f = 0; f < SetupFontsCountE; f++) {
    +                if (strcasecmp(value, sFontsArgs[f]) == 0) {
    +                    sSetup.fontsArg   = (SetupFontsE)f;
    +                    sSetup.fontsGiven = true;
    +                    sSetup.fontsBad   = false;
    +                }
    +            }
             }
         }
     
    @@ -1059,6 +1206,15 @@ static void showProgress(const PkgJobT *job) {
     
     
     // The uninstall page for sSetup.destDir.
    +// The room the fonts chosen need, on the folder page
    +static void showSpace(void) {
    +    char text[SETUP_MSG_MAX];
    +
    +    snprintf(text, sizeof(text), "DVX needs about %" PRIu64 " MB of disk space.", megabytes(setupSpaceNeeded(&sSetup.src.info, (SetupFontsE)wgtRadioGroupGetSelected(sSetup.fontsGroup))));
    +    wgtSetText(sSetup.spaceText, text);
    +}
    +
    +
     static void showUninstall(void) {
         char msg[SETUP_MSG_MAX];
     
    @@ -1083,6 +1239,12 @@ static void startDvx(void) {
     }
     
     
    +// The font of a use (the system font when it did not open)
    +static const BitmapFontT *typeFont(SetupTypeE type) {
    +    return sSetup.fonts[type] ? sSetup.fonts[type] : &sSetup.ctx.font;
    +}
    +
    +
     // Uninstall confirmed: take the AUTOEXEC.BAT line out (asking), then
     // remove the files.
     static void uninstallRun(void) {
    @@ -1139,11 +1301,17 @@ int main(int argc, char *argv[]) {
         wgtRegisterCheckbox();
         wgtRegisterLabel();
         wgtRegisterProgress();
    +    wgtRegisterRadio();
         wgtRegisterSpacer();
         wgtRegisterTextInput();
         sBoardType = wgtRegisterClass(&sClassBoard);
         pkgFind(sSetup.exePath, &sSetup.src);
    -    dvxLog("Setup: %s, package %s (%d disk(s), %d files)", sSetup.exePath, sFoundNames[sSetup.src.found], (int)sSetup.src.info.diskCount, (int)sSetup.src.info.fileCount);
    +    dvxLog("Setup: %s, package %s (%d disk(s), %d files)", sSetup.exePath, sFoundNames[sSetup.src.found], (int)sSetup.src.info.diskCount, (int)sSetup.src.info.tiers[PKG_TIERS - 1].fileCount);
    +
    +    if (sSetup.fontsBad) {
    +        fprintf(stderr, "DVX Setup: %s takes NONE, BASE, or ALL.\n", SETUP_ARG_FONTS);
    +        return SETUP_EXIT_FAILED;
    +    }
     
         // Without a package Setup can only uninstall the DVX in its own folder.
         char list[DVX_MAX_PATH];
    @@ -1159,7 +1327,9 @@ int main(int argc, char *argv[]) {
             return SETUP_EXIT_FAILED;
         }
     
    -    // The window under the splash's title, centred across the screen
    +    // The fonts Setup carries, then the window under the splash's title,
    +    // centred across the screen
    +    fontsLoad();
         paintBackdrop();
         sSetup.win = dvxCreateWindow(&sSetup.ctx, SETUP_TITLE, (sSetup.ctx.display.width - SETUP_WIN_W) / 2, SPLASH_MARGIN + sSetup.ctx.font.charHeight * SPLASH_TITLE_SCALE + SETUP_WIN_GAP, SETUP_WIN_W, SETUP_WIN_H, false);
     
    diff --git a/src/installer/setupCore.c b/src/installer/setupCore.c
    index ad6cc10d..6fcc912c 100644
    --- a/src/installer/setupCore.c
    +++ b/src/installer/setupCore.c
    @@ -57,11 +57,13 @@ bool           setupAutoexecLine(const char *destDir, char *out, int32_t outSize
     bool           setupAutoexecPath(char bootDrive, char *out, int32_t outSize);
     bool           setupAutoexecRemove(const char *path, const char *line, char *err, int32_t errSize);
     bool           setupCheckDest(const char *srcDir, const char *destDir, char *err, int32_t errSize);
    +SetupFontsE    setupFontsInstalled(const char *destDir, const PkgHeaderT *info);
     bool           setupIsInstalled(const char *destDir);
     bool           setupKeepSetting(const char *relName);
     bool           setupListWrite(const char *destDir, char *const *files, int32_t count);
    +void           setupRemoveDropped(const char *destDir, const char *folder, char *const *files, int32_t count);
     void           setupRemoveWritten(char *const *written, int32_t writtenCount, char *const *dirs, int32_t dirCount);
    -uint64_t       setupSpaceNeeded(const PkgHeaderT *info);
    +uint64_t       setupSpaceNeeded(const PkgHeaderT *info, SetupFontsE tier);
     bool           setupUninstall(const char *destDir, bool removeSettings, char *err, int32_t errSize);
     bool           setupWriteIni(const char *destDir, int32_t width, int32_t height, int32_t bpp);
     static bool    writeAll(FILE *f, const char *data, int32_t len);
    @@ -338,6 +340,30 @@ bool setupCheckDest(const char *srcDir, const char *destDir, char *err, int32_t
     }
     
     
    +SetupFontsE setupFontsInstalled(const char *destDir, const PkgHeaderT *info) {
    +    char **names = listRead(destDir);
    +
    +    if (!names) {
    +        return SetupFontsAllE;
    +    }
    +
    +    size_t   len   = strlen(SETUP_FONTS_FOLDER);
    +    uint32_t base  = info->tiers[SetupFontsBaseE].fileCount - info->tiers[SetupFontsNoneE].fileCount;
    +    uint32_t fonts = 0;
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(names); i++) {
    +        const char *name = names[i];
    +
    +        if (strncasecmp(name, SETUP_FONTS_FOLDER, len) == 0 && name[strlen(name) - 1] != PKG_NAME_SEP) {
    +            fonts++;
    +        }
    +    }
    +
    +    listFree(names);
    +    return (fonts == 0) ? SetupFontsNoneE : (fonts <= base) ? SetupFontsBaseE : SetupFontsAllE;
    +}
    +
    +
     bool setupIsInstalled(const char *destDir) {
         char        path[DVX_MAX_PATH];
         struct stat st;
    @@ -400,6 +426,49 @@ bool setupListWrite(const char *destDir, char *const *files, int32_t count) {
     }
     
     
    +void setupRemoveDropped(const char *destDir, const char *folder, char *const *files, int32_t count) {
    +    char  **old  = listRead(destDir);
    +    char  **dirs = NULL;
    +    size_t  len  = strlen(folder);
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(old); i++) {
    +        char path[DVX_MAX_PATH];
    +        bool kept = false;
    +
    +        for (int32_t j = 0; j < count && !kept; j++) {
    +            kept = strcasecmp(files[j], old[i]) == 0;
    +        }
    +
    +        if (kept || strncasecmp(old[i], folder, len) != 0 || !pkgLocalPath(destDir, old[i], path, sizeof(path))) {
    +            continue;
    +        }
    +
    +        // A folder: removed after the files, deepest first, when empty
    +        size_t pathLen = strlen(path);
    +
    +        if (platformPathIsSep(path[pathLen - 1])) {
    +            path[pathLen - 1] = '\0';
    +            arrput(dirs, strdup(path));
    +        } else {
    +            removeFile(path);
    +        }
    +    }
    +
    +    // dirs is still NULL when no folder was dropped; qsort's base must
    +    // not be NULL even for a zero count.
    +    if (dirs) {
    +        qsort(dirs, (size_t)arrlen(dirs), sizeof(char *), byLengthDesc);
    +    }
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(dirs); i++) {
    +        rmdir(dirs[i]);
    +    }
    +
    +    listFree(dirs);
    +    listFree(old);
    +}
    +
    +
     void setupRemoveWritten(char *const *written, int32_t writtenCount, char *const *dirs, int32_t dirCount) {
         for (int32_t i = 0; i < writtenCount; i++) {
             removeFile(written[i]);
    @@ -411,8 +480,10 @@ void setupRemoveWritten(char *const *written, int32_t writtenCount, char *const
     }
     
     
    -uint64_t setupSpaceNeeded(const PkgHeaderT *info) {
    -    return info->unpackedBytes + (uint64_t)info->fileCount * SETUP_FILE_SLACK;
    +uint64_t setupSpaceNeeded(const PkgHeaderT *info, SetupFontsE tier) {
    +    const PkgTierT *t = &info->tiers[tier];
    +
    +    return t->bytes + (uint64_t)t->fileCount * SETUP_FILE_SLACK;
     }
     
     
    diff --git a/src/installer/setupCore.h b/src/installer/setupCore.h
    index a3c1c565..414f8ea6 100644
    --- a/src/installer/setupCore.h
    +++ b/src/installer/setupCore.h
    @@ -24,7 +24,9 @@
     // the list of installed files, removing DVX, DVX.INI and AUTOEXEC.BAT
     // (host tested)
     //
    -// SETUP.EXE unpacks the DVX package (pkgRead.h) into the target folder.
    +// SETUP.EXE unpacks the DVX package (pkgRead.h) into the target folder:
    +// the program, and of the fonts none, the base set, or all (the package's
    +// tiers, pkgFormat.h).
     // On a folder that already holds DVX (its CONFIG\DVX.INI exists) an
     // upgrade keeps the files under CONFIG, so the user's settings, groups
     // and file types survive.  Every file the package put there is listed in
    @@ -36,6 +38,7 @@
     #define SETUP_CORE_H
     
     #include "pkgFormat.h"
    +#include "dvxText.h"
     
     #include 
     #include 
    @@ -52,6 +55,21 @@
     // '/' per line)
     #define SETUP_LIST_NAME      "SETUP.LST"
     
    +// What Setup installs of the fonts: the last of the package's tiers it
    +// unpacks (tier 0 the program, 1 the base set of fonts, 2 the others)
    +typedef enum {
    +    SetupFontsNoneE,
    +    SetupFontsBaseE,
    +    SetupFontsAllE,
    +    SetupFontsCountE
    +} SetupFontsE;
    +
    +_Static_assert(SetupFontsCountE == PKG_TIERS, "a fonts choice per package tier");
    +
    +// The fonts folder as SETUP.LST names it (an upgrade with fewer fonts
    +// removes the ones it leaves out: setupRemoveDropped)
    +#define SETUP_FONTS_FOLDER   DVX_FONTS_DIR "/"
    +
     // Room a file takes beyond its size on the target disk (half a 16 KB
     // cluster, typical of a FAT16 hard disk), for the free space check
     #define SETUP_FILE_SLACK     8192
    @@ -89,6 +107,11 @@ bool     setupAutoexecRemove(const char *path, const char *line, char *err, int3
     // package is read from nor inside it.  False with a reason in err.
     bool     setupCheckDest(const char *srcDir, const char *destDir, char *err, int32_t errSize);
     
    +// What of the fonts the DVX in destDir has, judged from its SETUP.LST and
    +// info's tiers: none, at most the base set's files, or more.  A folder
    +// without a list (a new install) gets all of them.
    +SetupFontsE setupFontsInstalled(const char *destDir, const PkgHeaderT *info);
    +
     // True when destDir already holds a DVX with settings (CONFIG\DVX.INI).
     bool     setupIsInstalled(const char *destDir);
     
    @@ -100,12 +123,18 @@ bool     setupKeepSetting(const char *relName);
     // cannot be written.
     bool     setupListWrite(const char *destDir, char *const *files, int32_t count);
     
    +// Remove the files under folder (a name ending in '/', as SETUP.LST has
    +// it) that destDir's list has and files (the new install's) does not --
    +// what an upgrade with fewer fonts leaves out -- then those of its
    +// folders left empty.  Call it before setupListWrite.
    +void     setupRemoveDropped(const char *destDir, const char *folder, char *const *files, int32_t count);
    +
     // Remove what a cancelled new install wrote: the files, then the folders
     // it made (newest first) when they are empty.
     void     setupRemoveWritten(char *const *written, int32_t writtenCount, char *const *dirs, int32_t dirCount);
     
    -// Bytes the package needs on the target disk.
    -uint64_t setupSpaceNeeded(const PkgHeaderT *info);
    +// Bytes tiers 0 .. tier of the package need on the target disk.
    +uint64_t setupSpaceNeeded(const PkgHeaderT *info, SetupFontsE tier);
     
     // Remove DVX from destDir: every file in SETUP.LST (except the settings
     // under CONFIG unless removeSettings), the log, the list itself, then
    diff --git a/src/installer/setupFonts.ini b/src/installer/setupFonts.ini
    new file mode 100644
    index 00000000..15b4cc6a
    --- /dev/null
    +++ b/src/installer/setupFonts.ini
    @@ -0,0 +1,32 @@
    +; setupFonts.ini -- the fonts SETUP.EXE carries (made by mkfont at build
    +; time; src/installer/Makefile turns them into setupFonts.h): DejaVu
    +; Sans Bold for the tagline and the billboards' headlines (24 pixels)
    +; and the done page's banner (36), DejaVu Sans for the lines under them
    +; (14).  Smoothed (Setup runs at 16 bits per pixel when it can), with
    +; only the characters Setup shows.  Setup cannot use the fonts it
    +; installs: they are inside the package, and on floppies the base set is
    +; on the later disks.
    +
    +[fonts]
    +SETUPB = DejaVu Sans
    +SETUP  = DejaVu Sans
    +
    +[licences]
    +DEJAVU = dejavu/dejavu-fonts-ttf-2.37/LICENSE
    +
    +[SETUPB]
    +style    = bold
    +licence  = DEJAVU
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans-Bold.ttf
    +sizes    = 24 36
    +smooth   = all
    +hinting  = native
    +chars    = 20-7E
    +
    +[SETUP]
    +licence  = DEJAVU
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans.ttf
    +sizes    = 14
    +smooth   = all
    +hinting  = native
    +chars    = 20-7E
    diff --git a/src/libs/kpunch/audio/audio.dhs b/src/libs/kpunch/audio/audio.dhs
    index a1a6d038..4ddaaa8f 100644
    --- a/src/libs/kpunch/audio/audio.dhs
    +++ b/src/libs/kpunch/audio/audio.dhs
    @@ -68,7 +68,7 @@
     
     .h1 Sound Library
     
    -Sound for DVX programs through whatever sound hardware the computer has: sampled sound from WAV and VOC files, tones and noise, and streams a program feeds itself (music, speech). Every program opens its own audio context; any number of programs play at the same time through one mixer, and everything a program opened is stopped and released when it ends, normally, killed or crashed.
    +Sound for DVX programs through whatever sound hardware the computer has: sampled sound from WAV and VOC files, tones and noise, and streams a program feeds itself (music, speech). Every program opens its own audio context; any number of programs play at the same time through one mixer, and everything a program opened is stopped and released when it ends, normally, killed, or crashed.
     
     Header: audio/audio.h (programs), audio/audioDrv.h (sound drivers)
     
    @@ -88,13 +88,13 @@ if (voice < 0) {
     audioClose(audio);                              // or let the program end
     .endcode
     
    -The first audioOpen starts the sound device and the last audioClose stops it. In between, after AUDIO_IDLE_CLOSE_SEC (3) seconds with nothing playing, the device's output is closed so it costs no CPU time (desktop sounds would otherwise keep it mixing silence), and the next sound opens it again with the same driver before it starts, so nothing of it is lost; audioGetInfo still reports the device. Functions returning int32_t give AUDIO_OK or a voice id (above 0) on success and a negative AUDIO_ERR_* code on failure; audioErrorText says what a code means. audioGetInfo describes the device: the driver's name and description, the mixing rate it achieved, its output bits and channels, the voice limit, and whether it plays sampled sound (hasDac), has a hardware synthesizer (hasSynth) or plays tones on a hardware tone generator (hasTone, the PC speaker). audioGetStats counts mixed blocks and frames, stream underruns, clipped samples, dropped voices and the voices playing.
    +The first audioOpen starts the sound device and the last audioClose stops it. In between, after AUDIO_IDLE_CLOSE_SEC (3) seconds with nothing playing, the device's output is closed so it costs no CPU time (desktop sounds would otherwise keep it mixing silence), and the next sound opens it again with the same driver before it starts, so nothing of it is lost; audioGetInfo still reports the device. Functions returning int32_t give AUDIO_OK or a voice id (above 0) on success and a negative AUDIO_ERR_* code on failure; audioErrorText says what a code means. audioGetInfo describes the device: the driver's name and description, the mixing rate it achieved, its output bits and channels, the voice limit, and whether it plays sampled sound (hasDac), has a hardware synthesizer (hasSynth), or plays tones on a hardware tone generator (hasTone, the PC speaker). audioGetStats counts mixed blocks and frames, stream underruns, clipped samples, dropped voices, and the voices playing.
     
     None of the functions may be called from an interrupt (a timer-service callback).
     
     .h2 Samples and Voices
     
    -audioSampleLoad (file), audioSampleLoadMem (memory) and audioSampleCreate (raw PCM in an AudioFormatT: rate, 8 bits unsigned or 16 bits signed, 1 or 2 channels) make an AudioSampleT; audioSampleFree frees it and stops the voices playing it. audioPlay(sample, volume, pan, loop) plays it on a new voice; audioPlayFile loads a file and plays it, freeing the sample when the voice ends.
    +audioSampleLoad (file), audioSampleLoadMem (memory), and audioSampleCreate (raw PCM in an AudioFormatT: rate, 8 bits unsigned or 16 bits signed, 1 or 2 channels) make an AudioSampleT; audioSampleFree frees it and stops the voices playing it. audioPlay(sample, volume, pan, loop) plays it on a new voice; audioPlayFile loads a file and plays it, freeing the sample when the voice ends.
     
     .table
       Setting              Range
    @@ -104,13 +104,13 @@ audioSampleLoad (file), audioSampleLoadMem (memory) and audioSampleCreate (raw P
       pitch                AUDIO_PITCH_MIN (64) .. AUDIO_PITCH_MAX (1024), AUDIO_PITCH_NORMAL (256) = recorded speed
     .endtable
     
    -audioVoiceSetVolume, audioVoiceSetPan, audioVoiceSetPitch, audioVoiceStop and audioVoiceIsPlaying take the context and the voice id; a voice id of another context, or of a voice that has ended, gives AUDIO_ERR_VOICE. audioStopAll stops every sample and tone of a context.
    +audioVoiceSetVolume, audioVoiceSetPan, audioVoiceSetPitch, audioVoiceStop, and audioVoiceIsPlaying take the context and the voice id; a voice id of another context, or of a voice that has ended, gives AUDIO_ERR_VOICE. audioStopAll stops every sample and tone of a context.
     
    -audioSetEndFn(ctx, fn, userData) asks for a call when a voice of ctx ends, with the reason: AUDIO_END_FINISHED (played out), AUDIO_END_STOPPED (stopped, or its sample or stream went away) or AUDIO_END_DROPPED (the voice limit made room for a louder sound). The call comes from audioPump or the shell's idle time, as the program that owns the context, never from an interrupt. The mixer runs up to one buffer (about 50 ms) ahead of what is heard, so a notification can come that much before the sound has finished playing.
    +audioSetEndFn(ctx, fn, userData) asks for a call when a voice of ctx ends, with the reason: AUDIO_END_FINISHED (played out), AUDIO_END_STOPPED (stopped, or its sample or stream went away), or AUDIO_END_DROPPED (the voice limit made room for a louder sound). The call comes from audioPump or the shell's idle time, as the program that owns the context, never from an interrupt. The mixer runs up to one buffer (about 50 ms) ahead of what is heard, so a notification can come that much before the sound has finished playing.
     
     .h2 Tones
     
    -audioTone(ctx, wave, hz, volume, ms) plays a square (AUDIO_WAVE_SQUARE), triangle (AUDIO_WAVE_TRIANGLE) or noise (AUDIO_WAVE_NOISE) tone of hz (AUDIO_TONE_MIN_HZ 20 to AUDIO_TONE_MAX_HZ 20000) for ms milliseconds, or until stopped when ms is 0. For noise, hz is how fast the noise changes: higher sounds brighter. audioVoiceSetFrequency changes a playing tone. When the computer has an FM chip (and DVX.INI [audio] fmTones is not false), a square or triangle tone plays on a free FM voice (a square as a carrier modulated at twice its pitch, which gives odd harmonics like a square wave; a triangle as a sine) at up to the chip's highest note (about 6,200 Hz); when every FM voice is taken it is mixed like any sound. Its voice id, volume, pan, pitch and end notification work the same either way. On a device with a DAC tones are mixed like any sound; on the PC speaker in its tones mode the loudest square or triangle tone playing is sounded by the timer chip.
    +audioTone(ctx, wave, hz, volume, ms) plays a square (AUDIO_WAVE_SQUARE), triangle (AUDIO_WAVE_TRIANGLE), or noise (AUDIO_WAVE_NOISE) tone of hz (AUDIO_TONE_MIN_HZ 20 to AUDIO_TONE_MAX_HZ 20000) for ms milliseconds, or until stopped when ms is 0. For noise, hz is how fast the noise changes: higher sounds brighter. audioVoiceSetFrequency changes a playing tone. When the computer has an FM chip (and DVX.INI [audio] fmTones is not false), a square or triangle tone plays on a free FM voice (a square as a carrier modulated at twice its pitch, which gives odd harmonics like a square wave; a triangle as a sine) at up to the chip's highest note (about 6,200 Hz); when every FM voice is taken it is mixed like any sound. Its voice id, volume, pan, pitch, and end notification work the same either way. On a device with a DAC tones are mixed like any sound; on the PC speaker in its tones mode the loudest square or triangle tone playing is sounded by the timer chip.
     
     .h2 Note Queues
     
    @@ -146,11 +146,11 @@ audioFmWrite(audio, v, AUDIO_FM_KEY_BLOCK, AUDIO_FM_KEY_ON | (block << AUDIO_FM_
     audioFmFree(audio, v);                          // key off, back to everyone
     .endcode
     
    -An OPL2 (9 voices) or OPL3 (18 voices, each playing left, right or both) found by the opl driver is shared by every program: audioFmAlloc(ctx) takes a two-operator voice (the least recently given back first, so a released note's tail is cut last) and returns its id, AUDIO_ERR_NO_VOICE when all are taken, AUDIO_ERR_BUSY while a program has the whole chip, AUDIO_ERR_NO_SYNTH without a chip. audioFmWrite(ctx, voice, reg, value) sets a register of the voice's channel, numbered as for the first channel of an OPL2 (AUDIO_FM_MOD_CHAR 20h, AUDIO_FM_CAR_CHAR 23h, levels 40h/43h, attack 60h/63h, sustain 80h/83h, waveforms E0h/E3h, AUDIO_FM_FNUM_LOW A0h, AUDIO_FM_KEY_BLOCK B0h, AUDIO_FM_FEEDBACK C0h); the library adds the channel's offsets and bank. On an OPL3 a feedback value without an output bit (AUDIO_FM_OUT_LEFT, AUDIO_FM_OUT_RIGHT) plays on both sides. Frequencies: hz = fnum x AUDIO_FM_CHIP_HZ (49,716) x 2^block / 2^20. audioFmFree keys the voice off and gives it back; closing the context gives back all of its voices. audioFmGetInfo tells the chip, its voices and how many are free.
    +An OPL2 (9 voices) or OPL3 (18 voices, each playing left, right, or both) found by the opl driver is shared by every program: audioFmAlloc(ctx) takes a two-operator voice (the least recently given back first, so a released note's tail is cut last) and returns its id, AUDIO_ERR_NO_VOICE when all are taken, AUDIO_ERR_BUSY while a program has the whole chip, AUDIO_ERR_NO_SYNTH without a chip. audioFmWrite(ctx, voice, reg, value) sets a register of the voice's channel, numbered as for the first channel of an OPL2 (AUDIO_FM_MOD_CHAR 20h, AUDIO_FM_CAR_CHAR 23h, levels 40h/43h, attack 60h/63h, sustain 80h/83h, waveforms E0h/E3h, AUDIO_FM_FNUM_LOW A0h, AUDIO_FM_KEY_BLOCK B0h, AUDIO_FM_FEEDBACK C0h); the library adds the channel's offsets and bank. On an OPL3 a feedback value without an output bit (AUDIO_FM_OUT_LEFT, AUDIO_FM_OUT_RIGHT) plays on both sides. Frequencies: hz = fnum x AUDIO_FM_CHIP_HZ (49,716) x 2^block / 2^20. audioFmFree keys the voice off and gives it back; closing the context gives back all of its voices. audioFmGetInfo tells the chip, its voices, and how many are free.
     
    -audioFmRawAcquire(ctx) gives one program the whole chip: every voice the others hold is keyed off and taken back (their tones go on in the mixer), new voices are refused, and audioFmRawWrite(ctx, reg, value) sets any register (bank x AUDIO_FM_BANK_REGS + index). A second program asking gets AUDIO_ERR_BUSY. audioFmRawRelease (or closing the context) silences the chip, sets it up again and gives its voices back to everyone.
    +audioFmRawAcquire(ctx) gives one program the whole chip: every voice the others hold is keyed off and taken back (their tones go on in the mixer), new voices are refused, and audioFmRawWrite(ctx, reg, value) sets any register (bank x AUDIO_FM_BANK_REGS + index). A second program asking gets AUDIO_ERR_BUSY. audioFmRawRelease (or closing the context) silences the chip, sets it up again, and gives its voices back to everyone.
     
    -audioFmAlloc, audioFmFree, audioFmWrite and audioFmRawWrite may also be called from a timer callback (platformTimerAdd); music.lib plays MIDI that way.
    +audioFmAlloc, audioFmFree, audioFmWrite, and audioFmRawWrite may also be called from a timer callback (platformTimerAdd); music.lib plays MIDI that way.
     
     .h2 MIDI Port
     
    @@ -162,11 +162,11 @@ audioSetMasterVolume sets the volume of all sound (DVX.INI [audio] volume, in pe
     
     .h2 Files
     
    -WAV: uncompressed PCM, 8-bit unsigned or 16-bit signed, mono or stereo, 4000 to 48000 Hz; WAVE_FORMAT_EXTENSIBLE with the PCM sub-format is accepted, other chunks are skipped, and a data chunk that claims more bytes than the file holds plays what is there. VOC (Creative Voice File, header checksum checked): sound blocks with 8-bit unsigned samples, continuation, silence, extended (rate and stereo for the next sound block) and new-format blocks with 8-bit or 16-bit PCM; markers and text are skipped, and a repeated section plays once. Compressed sound (ADPCM, 24-bit, floating point) gives AUDIO_ERR_UNSUPPORTED, a damaged file AUDIO_ERR_CORRUPT, a file that is neither AUDIO_ERR_FORMAT.
    +WAV: uncompressed PCM, 8-bit unsigned or 16-bit signed, mono or stereo, 4000 to 48000 Hz; WAVE_FORMAT_EXTENSIBLE with the PCM sub-format is accepted, other chunks are skipped, and a data chunk that claims more bytes than the file holds plays what is there. VOC (Creative Voice File, header checksum checked): sound blocks with 8-bit unsigned samples, continuation, silence, extended (rate and stereo for the next sound block), and new-format blocks with 8-bit or 16-bit PCM; markers and text are skipped, and a repeated section plays once. Compressed sound (ADPCM, 24-bit, floating point) gives AUDIO_ERR_UNSUPPORTED, a damaged file AUDIO_ERR_CORRUPT, a file that is neither AUDIO_ERR_FORMAT.
     
     .h2 The Mixer
     
    -All sound is mixed in 16-bit integers at the rate the device achieved (the mixer never assumes the rate it asked for: a Sound Blaster Pro asked for 22,050 Hz plays at 22,222 Hz), and every source is resampled to it with linear interpolation. Gains combine volume, pan, context volume and master volume; the sum is clipped to 16 bits. The mixer runs from the sound card's own interrupt (the Sound Blaster, at the end of each half buffer of about 23 ms), or from the timer service for devices without one, so sound keeps playing however busy the programs are. It uses no floating point and touches only memory pinned for interrupts (sample and stream buffers are pinned when they are made).
    +All sound is mixed in 16-bit integers at the rate the device achieved (the mixer never assumes the rate it asked for: a Sound Blaster Pro asked for 22,050 Hz plays at 22,222 Hz), and every source is resampled to it with linear interpolation. Gains combine volume, pan, context volume, and master volume; the sum is clipped to 16 bits. The mixer runs from the sound card's own interrupt (the Sound Blaster, at the end of each half buffer of about 23 ms), or from the timer service for devices without one, so sound keeps playing however busy the programs are. It uses no floating point and touches only memory pinned for interrupts (sample and stream buffers are pinned when they are made).
     
     .h2 Settings
     
    @@ -177,8 +177,8 @@ DVX.INI section [audio]:
       ---          -------
       driver       auto (default: find the hardware), or a driver name: sb, speaker, null
       rate         mixing rate asked for, 4000 to 48000 (default 22050)
    -  bits         0 (the device's best), 8 or 16
    -  channels     0 (the device's best), 1 or 2
    +  bits         0 (the device's best), 8, or 16
    +  channels     0 (the device's best), 1, or 2
       voices       voices mixed at once, 1 to 32 (default 16)
       volume       master volume in percent (default 100)
       speakerPcm   true: sampled sound on the PC speaker (see below); default false
    @@ -186,7 +186,7 @@ DVX.INI section [audio]:
       oplPort      the FM chip's port, hex (default 388)
       midi         where MIDI music plays: auto (default: the FM chip, else music.lib's software synthesizer), mpu401, fm, software
       mpuPort      the MPU-401's port, hex (default 330); the port is only looked for with midi = mpu401
    -  mpuMode      what is on the MPU-401 (music.lib): gm (default), mt32 or raw
    +  mpuMode      what is on the MPU-401 (music.lib): gm (default), mt32, or raw
       midiBank     the OP2 instrument bank for FM MIDI (music.lib), a path from the DVX folder
     .endtable
     
    @@ -215,7 +215,7 @@ The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h)
     .table
       Driver       Device
       ------       ------
    -  sb           Sound Blaster family by DSP version: 1.x (8-bit single-cycle), 1.5 / 2.0 (8-bit auto-init, high-speed above 23,000 Hz on 2.0), Pro and Pro 2 (8-bit stereo at 11,025 or 22,050 Hz), 16 (8- or 16-bit, mono or stereo, any rate from 5,000 to 44,100 Hz). The port, IRQ and DMA channels come from the BLASTER variable (e.g. SET BLASTER=A220 I5 D1 H5); without it an SB16 is found at 220h or 240h and asked for its IRQ and DMA. If no interrupt arrives from the card within half a second (a wrong IRQ), the next driver is tried.
    +  sb           Sound Blaster family by DSP version: 1.x (8-bit single-cycle), 1.5 / 2.0 (8-bit auto-init, high-speed above 23,000 Hz on 2.0), Pro and Pro 2 (8-bit stereo at 11,025 or 22,050 Hz), 16 (8- or 16-bit, mono or stereo, any rate from 5,000 to 44,100 Hz). The port, IRQ, and DMA channels come from the BLASTER variable (e.g. SET BLASTER=A220 I5 D1 H5); without it an SB16 is found at 220h or 240h and asked for its IRQ and DMA. If no interrupt arrives from the card within half a second (a wrong IRQ), the next driver is tried.
       speaker      The PC speaker. By default tones only: the timer chip plays the loudest square or triangle tone as a square wave, with no interrupts and no CPU cost; sampled sound and noise are not heard. With speakerPcm = true it plays everything by pulse-width modulation: the timer interrupt runs at 22,096 Hz and each interrupt sets the next pulse (about 5.7 bits). That costs a lot of CPU time: 22,096 interrupts a second plus the mixing. A 486DX2-66 spends a large share of its time on it (estimated 10 to 15 percent for the interrupts alone on plain DOS, much more under a memory manager or Windows) and can lose interrupts, which slows the sound and the DVX clock down. Meant for Pentium-class machines.
       null         No sound device: plays nothing, but voices still run their course and end, so programs behave the same.
       opl          (synthesizer) The OPL2 or OPL3 FM chip of an AdLib or Sound Blaster at 388h (oplPort), found by the AdLib timer test; the status register tells an OPL2 from an OPL3. Register writes are timed by status reads (3.3 and 23 us on an OPL2) with interrupts off around each index/data pair. Closing zeroes every register.
    @@ -228,7 +228,7 @@ Sound drivers are tried in this order: the one DVX.INI names, then from the high
     
     .h2 Writing a Driver
     
    -A driver is a driver module (see dvxDrv.h in the API reference): .drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers// folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it.
    +A driver is a driver module (see dvxDrv.h in the API reference): .drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers// folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX, or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it.
     
     A synthesizer driver exports an AudioSynthDriverT named audioSynth instead, written with AUDIO_SYNTH_DRIVER(name, priority, .kind = AUDIO_SYNTH_FM or AUDIO_SYNTH_MIDI, .open = ..., .close = ..., .write = ... or .put = ...). open detects the hardware, fills AudioSynthDrvInfoT (description; for FM the voices and whether it is stereo) and leaves it silent; close silences it. write(reg, value) sets an FM register (bank x AUDIO_FM_BANK_REGS + index) and must keep the index/data pair together (audioIrqOff / audioIrqRestore); put(byte) sends a MIDI byte, returning false when the port is not ready. Both run at interrupt time too (timer callbacks), so they follow the rules above and are pinned by the driver.
     
    @@ -236,8 +236,8 @@ A synthesizer driver exports an AudioSynthDriverT named audioSynth instead, writ
     
     src/include/basic/sound.bas (DECLARE LIBRARY "audio"): PlayWave(fileName), PlayWaveLoop(fileName), PlayTone(frequency, milliseconds, volume), PlayNoise(frequency, milliseconds, volume) return a voice number (0 on failure: SoundError$ says why); StopWave(voice), StopAllSounds, SoundPlaying(voice), SoundVolume(percent), SoundInfo$ (the device, e.g. "Sound Blaster 16 (DSP 4.05) at 220h, 22050 Hz, 16-bit stereo"). Volumes are percent. Each BASIC program plays through its own context, closed when it ends.
     
    -For a settings program (the Control Panel's Sounds page): SoundEventCount, SoundEventLabel$(eventIdx), SoundEventKey$(eventIdx) (audioDeskLabel / audioDeskKey; events numbered from 0 in DvxSoundE order); SoundEventFile$(prefs, eventIdx) and SoundSetEventFile(prefs, eventIdx, fileName) (audioDeskFile / audioDeskSetFile); SoundEventsReload (audioDeskReload); SoundTestFile(fileName) (audioDeskPlayFile: a voice number, or 0 with SoundError$); SoundFolder$ (AUDIO_SOUNDS_DIR); SoundMasterVolume and SoundSetMasterVolume(percent) (audioGetMasterVolume / audioSetMasterVolume in percent); SoundDriverCount (reads audioDriverNames, at most AUDIO_DRIVERS_MAX) and SoundDriverName$(index); SoundDriverAuto$ (AUDIO_DRIVER_AUTO); SoundMidiCount and SoundMidiLabel$(index) (audioMidiChoices); SoundSettingDriver$(prefs), SoundSettingMidi(prefs), SoundSettingVolume(prefs) and SoundSettingsStore(prefs, driver, midi, volume) (the audioSetting functions). prefs is a prefs.bas handle (prefsBasHandle, dvxPrefs.h); a handle that is not open reads the defaults and stores nothing.
    +For a settings program (the Control Panel's Sounds page): SoundEventCount, SoundEventLabel$(eventIdx), SoundEventKey$(eventIdx) (audioDeskLabel / audioDeskKey; events numbered from 0 in DvxSoundE order); SoundEventFile$(prefs, eventIdx) and SoundSetEventFile(prefs, eventIdx, fileName) (audioDeskFile / audioDeskSetFile); SoundEventsReload (audioDeskReload); SoundTestFile(fileName) (audioDeskPlayFile: a voice number, or 0 with SoundError$); SoundFolder$ (AUDIO_SOUNDS_DIR); SoundMasterVolume and SoundSetMasterVolume(percent) (audioGetMasterVolume / audioSetMasterVolume in percent); SoundDriverCount (reads audioDriverNames, at most AUDIO_DRIVERS_MAX) and SoundDriverName$(index); SoundDriverAuto$ (AUDIO_DRIVER_AUTO); SoundMidiCount and SoundMidiLabel$(index) (audioMidiChoices); SoundSettingDriver$(prefs), SoundSettingMidi(prefs), SoundSettingVolume(prefs), and SoundSettingsStore(prefs, driver, midi, volume) (the audioSetting functions). prefs is a prefs.bas handle (prefsBasHandle, dvxPrefs.h); a handle that is not open reads the defaults and stores nothing.
     
    -The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running" True / False; "voices" = voiceLimit, "rate", "bits", "channels"; any case, 0 for another field); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order). Samples: SoundLoad(fileName) (audioSampleLoad) and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped) and SoundSetFrequency(voice, hz) (the audioVoiceSet functions; the result is kept for SoundError$). Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamClose. FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS). The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease. CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*) and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*).
    +The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running" True / False; "voices" = voiceLimit, "rate", "bits", "channels"; any case, 0 for another field); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order). Samples: SoundLoad(fileName) (audioSampleLoad) and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped), and SoundSetFrequency(voice, hz) (the audioVoiceSet functions; the result is kept for SoundError$). Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamClose. FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS). The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease. CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*), and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*).
     
     Sound and stream numbers are 1..64 per kind, valid only for the program that got them; audioClose of a BASIC context forgets its numbers (audioBasForget) as it frees the samples and streams. BINARY STRING parameters arrive as a pointer and a byte count (two C parameters; see the DECLARE LIBRARY rules in the BASIC reference).
    diff --git a/src/libs/kpunch/conn/conn.dhs b/src/libs/kpunch/conn/conn.dhs
    index 5dbe67e1..6de169b7 100644
    --- a/src/libs/kpunch/conn/conn.dhs
    +++ b/src/libs/kpunch/conn/conn.dhs
    @@ -34,9 +34,9 @@
     
     .h1 Connection Library
     
    -One interface for byte-stream connections: a serial port, a modem with an AT command dialer, the secure link to the DVX host proxy (over a COM port or over the network), and Telnet. Every call except the host name lookup is non-blocking: connOpen starts the connection (dialing, TCP connect, key exchange) and connPoll moves it along, fills the receive buffer and sends what connWrite queued. Call connPoll as often as you can (every frame). The lookup of a host name waits for the answer, but other programs keep running meanwhile; a dotted address needs no lookup.
    +One interface for byte-stream connections: a serial port, a modem with an AT command dialer, the secure link to the DVX host proxy (over a COM port or over the network), and Telnet. Every call except the host name lookup is non-blocking: connOpen starts the connection (dialing, TCP connect, key exchange) and connPoll moves it along, fills the receive buffer, and sends what connWrite queued. Call connPoll as often as you can (every frame). The lookup of a host name waits for the answer, but other programs keep running meanwhile; a dotted address needs no lookup.
     
    -Loaded as: bin/libs/kpunch/conn/conn.lib (needs the serial, networking and transfer libraries; the transfer library for its BASIC functions)
    +Loaded as: bin/libs/kpunch/conn/conn.lib (needs the serial, networking, and transfer libraries; the transfer library for its BASIC functions)
     
     Header: conn/conn.h
     
    @@ -45,8 +45,8 @@ Header: conn/conn.h
     .table
       Type         Behaviour
       ----         ---------
    -  ConnSerialE  A COM port with the given speed, data bits, parity, stop bits and flow control (none, XON/XOFF, RTS/CTS). Open at once.
    -  ConnModemE   The COM port, then the init string (waits for OK), then dial prefix + phone number. CONNECT opens the connection; BUSY, NO ANSWER and NO CARRIER dial again after redialDelay seconds, up to redialCount more times; NO DIAL TONE and ERROR end it. An empty phone number opens the port for typing AT commands. When the carrier (DCD) has been seen and drops, the connection ends. Closing drops DTR, which makes the modem hang up. Everything the modem says is also delivered as received data.
    +  ConnSerialE  A COM port with the given speed, data bits, parity, stop bits, and flow control (none, XON/XOFF, RTS/CTS). Open at once.
    +  ConnModemE   The COM port, then the init string (waits for OK), then dial prefix + phone number. CONNECT opens the connection; BUSY, NO ANSWER, and NO CARRIER dial again after redialDelay seconds, up to redialCount more times; NO DIAL TONE and ERROR end it. An empty phone number opens the port for typing AT commands. When the carrier (DCD) has been seen and drops, the connection ends. Closing drops DTR, which makes the modem hang up. Everything the modem says is also delivered as received data.
       ConnSecureE  The secure link (secLink): the key exchange runs in the background, authenticated when a passphrase is set (both sides must use the same one, or none). Only encrypted, authenticated data on channel 0 is delivered. With host empty it runs on the COM port; with host set it runs over a TCP connection to host:tcpPort (the proxy's listening port, 2323 by default), through secLinkOpenStream.
       ConnTelnetE  TCP to host:tcpPort with Telnet (RFC 854). Negotiated: the host may ECHO and SUPPRESS-GO-AHEAD; BINARY both ways; TERMINAL-TYPE answered "ANSI"; NAWS sends the size given to connSetWindowSize (80 x 25 until then, sent again on every change). Other options are refused, and an option is only answered when its state changes. 0xFF is doubled on the way out and undoubled on the way in; outside binary mode a lone CR goes out as CR NUL and CR NUL comes in as CR.
     .endtable
    @@ -60,7 +60,7 @@ The Telnet protocol itself is a separate filter (connTelnet.c) that only moves b
       -----                     -------
       type                      ConnTypeE.
       comPort                   RS232_COM1 .. RS232_COM4.
    -  bps, dataBits, parity     Line settings (parity 'N', 'E', 'O', 'M' or 'S').
    +  bps, dataBits, parity     Line settings (parity 'N', 'E', 'O', 'M', or 'S').
       stopBits, flow            1 or 2 stop bits; RS232_HANDSHAKE_* flow control.
       initString, dialPrefix    Modem: sent before dialing (empty = none); dial command such as ATDT.
       phone                     Modem: the number (empty = no dialing).
    @@ -77,14 +77,14 @@ The Telnet protocol itself is a separate filter (connTelnet.c) that only moves b
       ConnT *connCreate(void)          Allocate a closed connection.
       void connDestroy(c)              Close and free.
       void connConfigDefaults(cfg)     COM1, 9600 8N1, no flow control, ATZ, ATDT.
    -  int32_t connOpen(c, cfg, nowMs)  Start connecting (cfg is copied). CONN_OK, CONN_ERR_PARAM, CONN_ERR_PORT or CONN_ERR_HOST (the host was not found or refused at once); connStatus explains. Later failures (refused, no answer within 30 seconds, connection lost, host closed it) end the connection with the reason in connStatus.
    +  int32_t connOpen(c, cfg, nowMs)  Start connecting (cfg is copied). CONN_OK, CONN_ERR_PARAM, CONN_ERR_PORT, or CONN_ERR_HOST (the host was not found or refused at once); connStatus explains. Later failures (refused, no answer within 30 seconds, connection lost, host closed it) end the connection with the reason in connStatus.
       void connPoll(c, nowMs)          Dial, key exchange, receive, send, timers.
       int32_t connRead(c, buf, max)    Take received bytes (0 when none).
       int32_t connWrite(c, data, len)  Queue bytes to send; returns the count accepted (0 while not open or when the 8 KB send buffer is full).
       int32_t connPending(c)           Bytes queued (here and in the layer below) not yet sent.
       bool connSendBreak(c, nowMs)     Hold a break for 300 ms (serial and modem).
       void connClose(c)                Close; a modem hangs up.
    -  ConnStateE connState(c)          ConnClosedE, ConnOpeningE (dialing, key exchange) or ConnOpenE.
    +  ConnStateE connState(c)          ConnClosedE, ConnOpeningE (dialing, key exchange), or ConnOpenE.
       const char *connStatus(c)        What is happening, or why the connection ended, in plain words.
       uint32_t connStatusSerial(c)     Changes whenever the status text changes.
       void connSetWindowSize(c, cols, rows)  The terminal size for Telnet's NAWS (sent now if the host asked for it).
    @@ -109,9 +109,9 @@ connPoll(c, nowMs());
     n = connRead(c, buf, sizeof(buf));
     .endcode
     
    -A Telnet session is the same with cfg.type = ConnTelnetE, cfg.host = "bbs.example.org" and cfg.tcpPort = 23.
    +A Telnet session is the same with cfg.type = ConnTelnetE, cfg.host = "bbs.example.org", and cfg.tcpPort = 23.
     
    -A file transfer runs over a connection by giving the transfer library connRead, connWrite and connPending as its XferIoT callbacks (ZMODEM over Telnet works: 0xFF bytes are escaped by the Telnet filter).
    +A file transfer runs over a connection by giving the transfer library connRead, connWrite, and connPending as its XferIoT callbacks (ZMODEM over Telnet works: 0xFF bytes are escaped by the Telnet filter).
     
     .topic lib.conn.basic
     .title Connections from BASIC
    @@ -151,7 +151,7 @@ Add conn.bas to a BASIC project (and xfer.bas for transfers): connBas.c gives BA
       ConnReceiveFiles(h, proto, target$, exists, escapeCtl)  Receive (exists: XFER_EXIST_).
     .endtable
     
    -ConnPoll with a Terminal attached does what the C Terminal's heartbeat did: it reads up to 4 KB, appends them to the capture file, answers each ENQ with the answerback (ENQ itself is not shown), stops at a ZMODEM start frame (the rest of what was read is dropped, and ConnZmodemRequest becomes True), and writes the rest to the Terminal. While a transfer started on the connection runs (or waits for XferAnswer), ConnPoll moves it along instead, nothing reaches the Terminal and typing is dropped; a transfer on a line that is no longer open is cancelled. The receive window of a ZMODEM download on a serial or modem line without flow control is 2 KB, so a slow frame never loses data, unless the program set one with ConnSetXferWindow.
    +ConnPoll with a Terminal attached does what the C Terminal's heartbeat did: it reads up to 4 KB, appends them to the capture file, answers each ENQ with the answerback (ENQ itself is not shown), stops at a ZMODEM start frame (the rest of what was read is dropped, and ConnZmodemRequest becomes True), and writes the rest to the Terminal. While a transfer started on the connection runs (or waits for XferAnswer), ConnPoll moves it along instead, nothing reaches the Terminal, and typing is dropped; a transfer on a line that is no longer open is cancelled. The receive window of a ZMODEM download on a serial or modem line without flow control is 2 KB, so a slow frame never loses data, unless the program set one with ConnSetXferWindow.
     
    -The Terminal binding goes through the form runtime (basFormRtTermBind, basFormRtTermUnbind and basFormRtTermForget in basrt, looked up when ConnAttach first needs them): the runtime unbinds a Terminal whose form unloads, and tells the library, so a connection never writes to a freed control. Every handle belongs to the program that made it; whatever it leaves open is closed (a modem hangs up, a capture file is closed, a transfer cancelled) when it ends or is ended.
    +The Terminal binding goes through the form runtime (basFormRtTermBind, basFormRtTermUnbind, and basFormRtTermForget in basrt, looked up when ConnAttach first needs them): the runtime unbinds a Terminal whose form unloads, and tells the library, so a connection never writes to a freed control. Every handle belongs to the program that made it; whatever it leaves open is closed (a modem hangs up, a capture file is closed, a transfer cancelled) when it ends or is ended.
     
    diff --git a/src/libs/kpunch/dvxnet/dvxnet.dhs b/src/libs/kpunch/dvxnet/dvxnet.dhs
    index 3af4dfd0..6da7e123 100644
    --- a/src/libs/kpunch/dvxnet/dvxnet.dhs
    +++ b/src/libs/kpunch/dvxnet/dvxnet.dhs
    @@ -36,12 +36,12 @@
     
     TCP/IP for DVX programs with the standard BSD socket API: a C programmer who knows sockets writes DVX network code the usual way. IPv4 today; the types and constants for IPv6 (AF_INET6, sockaddr_in6, sockaddr_storage) are defined so code can be written for both.
     
    -Loaded as: bin/libs/kpunch/dvxnet/dvxnet.lib (needs the shell, the task switcher and the core library)
    +Loaded as: bin/libs/kpunch/dvxnet/dvxnet.lib (needs the shell, the task switcher, and the core library)
     
    -Headers: dvxnet/dvxNet.h (settings and status), and the standard headers in dvxnet/include: sys/socket.h, netinet/in.h, netinet/tcp.h, arpa/inet.h, netdb.h, sys/select.h, poll.h, sys/ioctl.h and errno.h (the C library's plus the network codes). Put the include directory on the compiler's include path ahead of the C library's. In the SDK they are in include/net, and a project Makefile that sets NET = 1 gets them (see SDK Quick Start).
    +Headers: dvxnet/dvxNet.h (settings and status), and the standard headers in dvxnet/include: sys/socket.h, netinet/in.h, netinet/tcp.h, arpa/inet.h, netdb.h, sys/select.h, poll.h, sys/ioctl.h, and errno.h (the C library's plus the network codes). Put the include directory on the compiler's include path ahead of the C library's. In the SDK they are in include/net, and a project Makefile that sets NET = 1 gets them (see SDK Quick Start).
     
     .link lib.dvxnet.api The socket API
    -.link lib.dvxnet.waiting Waiting, polling and cooperative multitasking
    +.link lib.dvxnet.waiting Waiting, polling, and cooperative multitasking
     .link lib.dvxnet.config Settings and status (dvxNet.h)
     .link lib.dvxnet.inside How it works
     .link lib.dvxnet.basic Networking from BASIC
    @@ -124,7 +124,7 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     
     .h1 The Socket API
     
    -Standard names, arguments and meanings. A socket is an ordinary file descriptor: read(), write() and close() work on it, as do fcntl(F_GETFL/F_SETFL, O_NONBLOCK), ioctl(FIONBIO, FIONREAD), select() and poll().
    +Standard names, arguments, and meanings. A socket is an ordinary file descriptor: read(), write(), and close() work on it, as do fcntl(F_GETFL/F_SETFL, O_NONBLOCK), ioctl(FIONBIO, FIONREAD), select(), and poll().
     
     .table
       Function                                   Notes
    @@ -143,7 +143,7 @@ Standard names, arguments and meanings. A socket is an ordinary file descriptor:
       getnameinfo                                Numeric host and service; NI_NAMEREQD gives EAI_NONAME.
       gethostname                                The computer name from the network settings.
       inet_pton, inet_ntop                       AF_INET (AF_INET6: EAFNOSUPPORT).
    -  inet_addr, inet_aton, inet_ntoa            BSD forms (a.b.c.d, a.b.c, a.b, a; decimal, octal or hex).
    +  inet_addr, inet_aton, inet_ntoa            BSD forms (a.b.c.d, a.b.c, a.b, a; decimal, octal, or hex).
       htonl, htons, ntohl, ntohs                 Inline in ; also real functions for code built without optimisation.
     .endtable
     
    @@ -162,13 +162,13 @@ Sockets are real descriptors below FD_SETSIZE (256), at most 32 at a time. dup()
     .index tsYield
     .index netPoll
     
    -.h1 Waiting, Polling and Cooperative Multitasking
    +.h1 Waiting, Polling, and Cooperative Multitasking
     
    -DVX runs programs cooperatively, so a call that has to wait never stops the system: blocking connect, accept, recv, send (with a full buffer), select, poll and name lookups wait by running the network and then letting the other tasks run (tsYield), again and again, until they can return. Other programs, and other connections, keep working meanwhile.
    +DVX runs programs cooperatively, so a call that has to wait never stops the system: blocking connect, accept, recv, send (with a full buffer), select, poll, and name lookups wait by running the network and then letting the other tasks run (tsYield), again and again, until they can return. Other programs, and other connections, keep working meanwhile.
     
     The network also runs from the shell's idle time, so connections progress (and data arrives) while a program is busy with other things; every socket call runs it too. A program that polls non-blocking sockets in its own loop needs nothing else.
     
    -Right after the network starts, DHCP needs a moment to get an address. connect, sendto and name lookups wait for it (up to 20 seconds) instead of failing, so a program started together with DVX can connect at once.
    +Right after the network starts, DHCP needs a moment to get an address. connect, sendto, and name lookups wait for it (up to 20 seconds) instead of failing, so a program started together with DVX can connect at once.
     
     .h2 Sockets Belong to Programs
     
    @@ -186,12 +186,12 @@ Each socket belongs to the program that created it (accept gives the new socket
     
     .h1 Settings and Status (dvxNet.h)
     
    -The settings live in DVX.INI, section [network]: dhcp (true/false), address, netmask, gateway, dns1, dns2, hostname and vector (auto or 0x60-0x80). The Control Panel's Network page edits them.
    +The settings live in DVX.INI, section [network]: dhcp (true/false), address, netmask, gateway, dns1, dns2, hostname, and vector (auto or 0x60-0x80). The Control Panel's Network page edits them.
     
     .table
       Function                                  Description
       --------                                  -----------
    -  int32_t netStart(void)                    Start with the saved settings unless running: NET_OK, NET_ERR_NO_DRIVER (the stack runs anyway, for 127.0.0.1), NET_ERR_CONFIG or NET_ERR_NO_MEMORY. Socket calls do this themselves.
    +  int32_t netStart(void)                    Start with the saved settings unless running: NET_OK, NET_ERR_NO_DRIVER (the stack runs anyway, for 127.0.0.1), NET_ERR_CONFIG, or NET_ERR_NO_MEMORY. Socket calls do this themselves.
       void netStop(void)                        Close every socket and stop (the packet driver is released).
       int32_t netRestart(void)                  Stop, then start with the saved settings (after they changed).
       void netPoll(void)                        Take received frames, run the timers, send. Called from the shell's idle time and by every socket call.
    @@ -233,7 +233,7 @@ lwIP runs without threads (NO_SYS), configured only by port/lwipopts.h: TCP MSS
     
     The receive ring holds 16 frames. A burst larger than that while DVX is busy loses frames (counted as "lost" in the status); TCP sends them again.
     
    -On the host the same sources are built into the "net" test suite: a fake packet driver passes frames through the real ring to a second copy of the stack in another process, which plays DHCP, DNS and echo server.
    +On the host the same sources are built into the "net" test suite: a fake packet driver passes frames through the real ring to a second copy of the stack in another process, which plays DHCP, DNS, and echo server.
     
     .topic lib.dvxnet.basic
     .title Networking from BASIC
    @@ -242,7 +242,7 @@ On the host the same sources are built into the "net" test suite: a fake packet
     
     .h1 Networking from BASIC
     
    -net.bas declares simple functions over the socket API (netBas.c): HostLookup$, HostName$, NetAddress$, NetError$, TcpConnect, TcpListen, TcpAccept, TcpSend, TcpPending, TcpRecv$, TcpConnected, TcpShutdown, TcpPeer$, TcpLocalPort, TcpSetNoDelay, TcpSetKeepAlive, TcpClose, UdpOpen, UdpSend, UdpRecv$, UdpFrom$, UdpClose. A handle is the socket's descriptor + 1, so 0 means failure. Only TcpConnect (up to 20 seconds) and TcpSend wait; everything else returns at once, so a program polls from a Timer. TcpSend, TcpRecv$, UdpSend and UdpRecv$ pass their data as BINARY STRING (pointer and length), so every byte value goes through. See the BASIC language reference, Include Libraries.
    +net.bas declares simple functions over the socket API (netBas.c): HostLookup$, HostName$, NetAddress$, NetError$, TcpConnect, TcpListen, TcpAccept, TcpSend, TcpPending, TcpRecv$, TcpConnected, TcpShutdown, TcpPeer$, TcpLocalPort, TcpSetNoDelay, TcpSetKeepAlive, TcpClose, UdpOpen, UdpSend, UdpRecv$, UdpFrom$, UdpClose. A handle is the socket's descriptor + 1, so 0 means failure. Only TcpConnect (up to 20 seconds) and TcpSend wait; everything else returns at once, so a program polls from a Timer. TcpSend, TcpRecv$, UdpSend, and UdpRecv$ pass their data as BINARY STRING (pointer and length), so every byte value goes through. See the BASIC language reference, Include Libraries.
     
     .table
       BASIC                                   C it calls
    diff --git a/src/libs/kpunch/dvxshell/README.md b/src/libs/kpunch/dvxshell/README.md
    index 52dac490..b270386e 100644
    --- a/src/libs/kpunch/dvxshell/README.md
    +++ b/src/libs/kpunch/dvxshell/README.md
    @@ -22,7 +22,7 @@ program discovery, running programs and documents, and file associations.
     - `shellInf.h`   -- shell-internal interfaces exposed to apps
     - `shellAssoc.h` -- file associations (CONFIG/ASSOC.INI)
     - `shellProg.h`  -- installed programs: discovery, names, icons
    -- `shellRun.h`   -- running programs, documents and command lines; the Run dialog
    +- `shellRun.h`   -- running programs, documents, and command lines; the Run dialog
     - `shellApp.c`   -- app registry, load, unload, lifecycle
     - `shellAssoc.c` -- file association store
     - `shellInfo.c`  -- system info service
    diff --git a/src/libs/kpunch/dvxshell/dvxshell.dhs b/src/libs/kpunch/dvxshell/dvxshell.dhs
    index 614999fb..00b3d55b 100644
    --- a/src/libs/kpunch/dvxshell/dvxshell.dhs
    +++ b/src/libs/kpunch/dvxshell/dvxshell.dhs
    @@ -570,7 +570,7 @@ void shellRegisterAppCleanup(ShellAppCleanupFnT fn);
     void shellUnregisterAppCleanup(ShellAppCleanupFnT fn);
     .endcode
     
    -Register a function the shell calls with an app's id whenever that app ends -- normally, force-killed or after a crash -- once its windows are gone, next to the shell's own release of the app's widget classes and hotkeys. A library uses it to release what the app left open with it: the networking library closes the app's sockets this way. For library code that stays loaded; registering the same function again has no effect, and unregistering one that is not registered does nothing.
    +Register a function the shell calls with an app's id whenever that app ends -- normally, force-killed, or after a crash -- once its windows are gone, next to the shell's own release of the app's widget classes and hotkeys. A library uses it to release what the app left open with it: the networking library closes the app's sockets this way. For library code that stays loaded; registering the same function again has no effect, and unregistering one that is not registered does nothing.
     
     .h2 shellDesktopUpdate
     
    @@ -595,7 +595,7 @@ extern void (*shellCtrlEscFn)(AppContextT *ctx);
     void shellOpenTaskManager(AppContextT *ctx);
     .endcode
     
    -shellCtrlEscFn is set by the taskmgr DXE's constructor; NULL if the task manager is not loaded. shellOpenTaskManager calls it when it is set and does nothing otherwise; Ctrl+Esc, Program Manager's Help > Task Manager and BASIC's ShellTaskManager all open Task Manager through it.
    +shellCtrlEscFn is set by the taskmgr DXE's constructor; NULL if the task manager is not loaded. shellOpenTaskManager calls it when it is set and does nothing otherwise; Ctrl+Esc, Program Manager's Help > Task Manager, and BASIC's ShellTaskManager all open Task Manager through it.
     
     .topic shell.info
     .title System Information
    @@ -641,7 +641,7 @@ Return the formatted system information text. The returned pointer is valid for
     
     Header: dvxshell/shellProg.h
     
    -Finds the installed programs (every .app file under APPS, SHELL_PROG_DIR) and reads what they are called and what they look like, for launchers, file managers and setup programs. A program's display name and description are its "name" and "description" text resources (the capitalized file name when it has no name resource); its picture is its "icon32" resource.
    +Finds the installed programs (every .app file under APPS, SHELL_PROG_DIR) and reads what they are called and what they look like, for launchers, file managers, and setup programs. A program's display name and description are its "name" and "description" text resources (the capitalized file name when it has no name resource); its picture is its "icon32" resource.
     
     .table
       Constant                 Value      Meaning
    @@ -675,7 +675,7 @@ Every program under dir (NULL for SHELL_PROG_DIR) and its subfolders, except ski
     void shellProgramInfo(const char *path, ShellProgramT *out);
     .endcode
     
    -Fill out with the program's path, display name and description. Always succeeds: without resources (or for a file that is not a program) the name is the file name without its extension, first letter capitalized, and the description is empty.
    +Fill out with the program's path, display name, and description. Always succeeds: without resources (or for a file that is not a program) the name is the file name without its extension, first letter capitalized, and the description is empty.
     
     .h2 shellProgramIcon
     
    @@ -939,7 +939,7 @@ int32_t     ShellAssocRemove(const char *extension);
     const char *ShellAssocNormalize(const char *extension);
     .endcode
     
    -ShellAssocProgram is shellAssocLookup: the stored value, the program followed by ",args" when there is a template. ShellAssocCommand is the program part of shellAssocCommand for a document. ShellAssocCount, ShellAssocExt and ShellAssocValue enumerate shellAssocGet (0-based; "" past the end). ShellAssocSet, ShellAssocRemove and ShellAssocNormalize are shellAssocSet, shellAssocRemove and shellAssocNormalize ("" when the extension cannot be one). Every change is saved at once.
    +ShellAssocProgram is shellAssocLookup: the stored value, the program followed by ",args" when there is a template. ShellAssocCommand is the program part of shellAssocCommand for a document. ShellAssocCount, ShellAssocExt, and ShellAssocValue enumerate shellAssocGet (0-based; "" past the end). ShellAssocSet, ShellAssocRemove, and ShellAssocNormalize are shellAssocSet, shellAssocRemove, and shellAssocNormalize ("" when the extension cannot be one). Every change is saved at once.
     
     .h2 Running programs and DVX
     
    @@ -1017,7 +1017,7 @@ ShellProgramsFolder is SHELL_PROG_DIR (where ShellProgramScan("") looks and a Br
     
     .h2 A desktop in BASIC
     
    -A BASIC program can be the desktop ([shell] desktop, ShellSetDesktop): the shell exits DVX when it ends, so it needs no code for that; its Exit command confirms and calls ShellExitDvx. Its groups use the shell's program-group model (Program Groups: the same PROGMAN.INI format, rules and limits as Program Manager) through shell.bas, in ShellConfigFile$'s folder. An icon grid is drawn on a PictureBox with PaintPicture and pictures from LOADPICTURE of ShellItemIcon$ (a .app gives its icon), with MouseDown/MouseUp for selecting and dragging and KeyDown for the keyboard. The test fixture src/test/dos/fixtures/parpm does all of this.
    +A BASIC program can be the desktop ([shell] desktop, ShellSetDesktop): the shell exits DVX when it ends, so it needs no code for that; its Exit command confirms and calls ShellExitDvx. Its groups use the shell's program-group model (Program Groups: the same PROGMAN.INI format, rules, and limits as Program Manager) through shell.bas, in ShellConfigFile$'s folder. An icon grid is drawn on a PictureBox with PaintPicture and pictures from LOADPICTURE of ShellItemIcon$ (a .app gives its icon), with MouseDown/MouseUp for selecting and dragging and KeyDown for the keyboard. The test fixture src/test/dos/fixtures/parpm does all of this.
     
     .topic shell.groups
     .title Program Groups
    @@ -1036,7 +1036,7 @@ A BASIC program can be the desktop ([shell] desktop, ShellSetDesktop): the shell
     
     Header: dvxshell/pmModel.h; source pmModel.c (model) and pmModelBas.c (BASIC entry points, shell.bas).
     
    -Program Manager's groups, items and PROGMAN.INI, as plain data with no windows, part of the shell so the C Program Manager and a desktop written in BASIC share one model (the pm prefix names it). groups[PM_INSTALLED_IDX] is the automatic Installed Programs group (pmInstalledScan: every program under SHELL_PROG_DIR, sorted by name, never saved; pmInstalledChanged tells cheaply whether a program was added or removed); the others are the user's, saved by pmModelSave with a crash-safe rewrite in this layout:
    +Program Manager's groups, items, and PROGMAN.INI, as plain data with no windows, part of the shell so the C Program Manager and a desktop written in BASIC share one model (the pm prefix names it). groups[PM_INSTALLED_IDX] is the automatic Installed Programs group (pmInstalledScan: every program under SHELL_PROG_DIR, sorted by name, never saved; pmInstalledChanged tells cheaply whether a program was added or removed); the others are the user's, saved by pmModelSave with a crash-safe rewrite in this layout:
     
     .code
     [options]  minimizeOnRun, restoreAlone, autoArrange, view, group
    @@ -1078,6 +1078,9 @@ BASIC (shell.bas, DECLARE LIBRARY "dvxshell"): ShellGroupsLoad / Save / Free / S
     .index SysColorsStore
     .index SysSetWallpaper
     .index SysWallpaperStore
    +.index SysSetTextSmoothing
    +.index SysTextSmoothing
    +.index SysFontsStore
     .index SysSetMouse
     .index SysMouseStore
     .index SysMouseX
    @@ -1095,7 +1098,7 @@ BASIC (shell.bas, DECLARE LIBRARY "dvxshell"): ShellGroupsLoad / Save / Free / S
     
     Source: dvxshell/shellSysBas.c; BASIC declarations: src/include/basic/system.bas (DECLARE LIBRARY "dvxshell"); prototypes in shellApp.h.
     
    -What Control Panel changes, for DVX BASIC programs: the video mode, the system colors and themes, the wallpaper and the mouse, each applied at once to shellContext(). Only BASIC-marshalable types; True (-1) / False (0); colors are BASIC RGB() values (&HBBGGRR, converted with wgtBasicColor); lists are 0-based. Arguments named prefs are prefs.bas handles, resolved with prefsBasHandle (dvxPrefs.h); 0 or a closed handle makes a Store or Load do nothing (a Load returns False).
    +What Control Panel changes, for DVX BASIC programs: the video mode, the system colors and themes, the wallpaper, text smoothing, and the mouse, each applied at once to shellContext(). Only BASIC-marshalable types; True (-1) / False (0); colors are BASIC RGB() values (&HBBGGRR, converted with wgtBasicColor); lists are 0-based. Arguments named prefs are prefs.bas handles, resolved with prefsBasHandle (dvxPrefs.h); 0 or a closed handle makes a Store or Load do nothing (a Load returns False).
     
     .code
     int32_t     SysVideoModeCount(void);
    @@ -1120,6 +1123,10 @@ void        SysSetWallpaperMode(int32_t mode);
     int32_t     SysWallpaperMode(void);
     void        SysWallpaperStore(int32_t prefs);
     int32_t     SysWallpaperLoad(int32_t prefs);
    +void        SysSetTextSmoothing(int32_t on);
    +int32_t     SysTextSmoothing(void);
    +void        SysFontsStore(int32_t prefs);
    +void        SysFontsLoad(int32_t prefs);
     void        SysSetMouse(int32_t wheelDir, int32_t dblClickMs, int32_t accel, int32_t mickeys, int32_t wheelStep);
     int32_t     SysMouseWheelDir(void);
     int32_t     SysMouseDoubleClick(void);
    @@ -1159,6 +1166,9 @@ int32_t     SysMemoryFreeKb(void);
       SysSetWallpaperMode          dvxSetWallpaperMode with the value of prefsWallpaperChoices[mode]; other modes are ignored
       SysWallpaper / Mode          ctx->wallpaperPath; the prefsWallpaperChoices index of ctx->wallpaperMode
       SysWallpaperStore / Load     dvxWallpaperToPrefs / dvxWallpaperFromPrefs (Load: False when the image could not be loaded)
    +  SysSetTextSmoothing          dvxSetFontSmoothing (nonzero is on)
    +  SysTextSmoothing             ctx->fontSmoothing (the setting; text is smoothed only above 8 bits per pixel)
    +  SysFontsStore / Load         dvxFontsToPrefs / dvxFontsFromPrefs
       SysSetMouse                  dvxSetMouseConfig
       SysMouse*                    ctx->wheelDirection, dblClickMs, mouseAccel, mouseMickeys, wheelStep
       SysMouseStore / Load         dvxMouseToPrefs / dvxMouseFromPrefs
    @@ -1173,7 +1183,7 @@ int32_t     SysMemoryFreeKb(void);
     
     There is no binding for platformInjectMouse: on DOS it replaces the real mouse until the next call and has no way back to it, so a synthesized click would leave the pointer stuck. A program moves the pointer with SysSetMousePos and "clicks" a control by calling its event procedure or setting its value.
     
    -system.bas also holds MOUSE_WHEEL_STEP_MIN / MAX / DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS and MOUSE_SPEED_DEFAULT, and SYS_VIDEO_DEFAULT_WIDTH / HEIGHT / BPP (DVX_DEFAULT_VIDEO_W / H / BPP, the mode prefsGetVideoMode and safe mode fall back to), as CONSTs; the basdev test suite (caseIncludeConsts) checks each against dvxTypes.h.
    +system.bas also holds MOUSE_WHEEL_STEP_MIN / MAX / DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS, and MOUSE_SPEED_DEFAULT, and SYS_VIDEO_DEFAULT_WIDTH / HEIGHT / BPP (DVX_DEFAULT_VIDEO_W / H / BPP, the mode prefsGetVideoMode and safe mode fall back to), as CONSTs; the basdev test suite (caseIncludeConsts) checks each against dvxTypes.h.
     
     The Store / Load routines are the ones the shell uses at start (dvxMouseFromPrefs, dvxColorsFromPrefs, dvxWallpaperFromPrefs on DVX.INI) and Control Panel uses for OK and for its Cancel snapshot (an in-memory prefsCreate handle), so the INI keys are written in one place (dvxApp.c).
     
    diff --git a/src/libs/kpunch/dvxshell/shellApp.c b/src/libs/kpunch/dvxshell/shellApp.c
    index 3625d4ac..8f233837 100644
    --- a/src/libs/kpunch/dvxshell/shellApp.c
    +++ b/src/libs/kpunch/dvxshell/shellApp.c
    @@ -28,6 +28,7 @@
     #include "shellApp.h"
     #include "dvxDlg.h"
     #include "dvxPlat.h"
    +#include "dvxText.h"
     #include "stb_ds_wrap.h"
     
     #include 
    @@ -577,6 +578,7 @@ static int32_t refuseSecondCopy(AppContextT *ctx, const char *path) {
     static void releaseAppResources(ShellAppT *app) {
         wgtUnregisterAppClasses(app->appId);
         dvxUnregisterAppHotkeys(app->appId);
    +    dvxFontRemoveOwner(app->appId);
         dvxAppHungForget(app->appId);
         platformTimerRemoveOwner(app->appId);
     
    @@ -892,6 +894,10 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
         deriveConfigDir(app->dxeCtx, path);
         defaultHelpFile(app->dxeCtx);
     
    +    // The fonts the program carries (font resources), for it alone; gone
    +    // when it ends (releaseAppResources)
    +    dvxFontAddResources(id, app->path);
    +
         // Copy launch arguments (if any)
         if (args && args[0]) {
             snprintf(app->dxeCtx->args, sizeof(app->dxeCtx->args), "%s", args);
    @@ -915,6 +921,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
                 ctx->currentAppId = callerAppId;
                 dvxSetBusy(ctx, false);
                 dvxErrorBox(ctx, NULL, "Failed to create task for application.");
    +            dvxFontRemoveOwner(id);
                 free(app->dxeCtx);
                 app->dxeCtx = NULL;
                 app->state = AppStateFreeE;
    diff --git a/src/libs/kpunch/dvxshell/shellApp.h b/src/libs/kpunch/dvxshell/shellApp.h
    index 726ba483..595ca362 100644
    --- a/src/libs/kpunch/dvxshell/shellApp.h
    +++ b/src/libs/kpunch/dvxshell/shellApp.h
    @@ -598,6 +598,8 @@ int32_t     SysFontCharCount(void);
     int32_t     SysFontFirstChar(void);
     int32_t     SysFontGlyphRow(int32_t code, int32_t row);
     int32_t     SysFontHeight(void);
    +void        SysFontsLoad(int32_t prefs);
    +void        SysFontsStore(int32_t prefs);
     int32_t     SysFontWidth(void);
     int32_t     SysGetColor(int32_t id);
     int32_t     SysLoadTheme(const char *path);
    @@ -620,10 +622,12 @@ int32_t     SysSendKey(int32_t keyCode, int32_t shift);
     void        SysSetColor(int32_t id, int32_t rgb);
     void        SysSetMouse(int32_t wheelDir, int32_t dblClickMs, int32_t accel, int32_t mickeys, int32_t wheelStep);
     void        SysSetMousePos(int32_t x, int32_t y);
    +void        SysSetTextSmoothing(int32_t on);
     int32_t     SysSetVideoMode(int32_t width, int32_t height, int32_t bpp);
     int32_t     SysSetWallpaper(const char *path);
     void        SysSetWallpaperMode(int32_t mode);
     int32_t     SysShiftState(void);
    +int32_t     SysTextSmoothing(void);
     int32_t     SysVideoModeBpp(int32_t index);
     int32_t     SysVideoModeCount(void);
     int32_t     SysVideoModeHeight(int32_t index);
    diff --git a/src/libs/kpunch/dvxshell/shellMain.c b/src/libs/kpunch/dvxshell/shellMain.c
    index daee8b4c..b3edeb1f 100644
    --- a/src/libs/kpunch/dvxshell/shellMain.c
    +++ b/src/libs/kpunch/dvxshell/shellMain.c
    @@ -784,8 +784,8 @@ int shellMain(int argc, char *argv[]) {
             // after dvxInit so the display fields have real values to report.
             shellInfoInit(&sCtx);
     
    -        // Apply the mouse, color and desktop preferences (the keys the
    -        // Control Panel writes; dvxApp.h)
    +        // Apply the mouse, color, font and desktop preferences (the keys
    +        // the Control Panel writes; dvxApp.h)
             dvxMouseFromPrefs(&sCtx, sPrefs);
             dvxLog("Preferences: mouse wheel=%ld doubleclick=%ldms accel=%ld speed=%ld wheelspeed=%ld", (long)sCtx.wheelDirection, (long)sCtx.dblClickMs, (long)sCtx.mouseAccel, (long)sCtx.mouseMickeys, (long)sCtx.wheelStep);
     
    @@ -793,6 +793,9 @@ int shellMain(int argc, char *argv[]) {
                 dvxLog("Preferences: loaded custom color scheme");
             }
     
    +        dvxFontsFromPrefs(&sCtx, sPrefs);
    +        dvxLog("Preferences: text smoothing %s", sCtx.fontSmoothing ? "on" : "off");
    +
             if (!dvxWallpaperFromPrefs(&sCtx, sPrefs)) {
                 dvxLog("Preferences: failed to load the wallpaper");
             } else if (sCtx.wallpaperPath[0]) {
    diff --git a/src/libs/kpunch/dvxshell/shellSysBas.c b/src/libs/kpunch/dvxshell/shellSysBas.c
    index 33dd49f3..c77766a5 100644
    --- a/src/libs/kpunch/dvxshell/shellSysBas.c
    +++ b/src/libs/kpunch/dvxshell/shellSysBas.c
    @@ -84,6 +84,8 @@ int32_t                      SysFontCharCount(void);
     int32_t                      SysFontFirstChar(void);
     int32_t                      SysFontGlyphRow(int32_t code, int32_t row);
     int32_t                      SysFontHeight(void);
    +void                         SysFontsLoad(int32_t prefs);
    +void                         SysFontsStore(int32_t prefs);
     int32_t                      SysFontWidth(void);
     int32_t                      SysGetColor(int32_t id);
     int32_t                      SysLoadTheme(const char *path);
    @@ -106,10 +108,12 @@ int32_t                      SysSendKey(int32_t keyCode, int32_t shift);
     void                         SysSetColor(int32_t id, int32_t rgb);
     void                         SysSetMouse(int32_t wheelDir, int32_t dblClickMs, int32_t accel, int32_t mickeys, int32_t wheelStep);
     void                         SysSetMousePos(int32_t x, int32_t y);
    +void                         SysSetTextSmoothing(int32_t on);
     int32_t                      SysSetVideoMode(int32_t width, int32_t height, int32_t bpp);
     int32_t                      SysSetWallpaper(const char *path);
     void                         SysSetWallpaperMode(int32_t mode);
     int32_t                      SysShiftState(void);
    +int32_t                      SysTextSmoothing(void);
     int32_t                      SysVideoModeBpp(int32_t index);
     int32_t                      SysVideoModeCount(void);
     int32_t                      SysVideoModeHeight(int32_t index);
    @@ -270,6 +274,27 @@ int32_t SysFontHeight(void) {
     }
     
     
    +// Apply the [fonts] settings of a prefs handle (text smoothing on when
    +// the key is missing).
    +void SysFontsLoad(int32_t prefs) {
    +    PrefsHandleT *h = prefsBasHandle(prefs);
    +
    +    if (h) {
    +        dvxFontsFromPrefs(shellContext(), h);
    +    }
    +}
    +
    +
    +// Store the font settings in [fonts] of a prefs handle.
    +void SysFontsStore(int32_t prefs) {
    +    PrefsHandleT *h = prefsBasHandle(prefs);
    +
    +    if (h) {
    +        dvxFontsToPrefs(shellContext(), h);
    +    }
    +}
    +
    +
     // The system font's character cell: every glyph is this wide and high.
     int32_t SysFontWidth(void) {
         return fontOf()->charWidth;
    @@ -441,6 +466,13 @@ void SysSetMousePos(int32_t x, int32_t y) {
     }
     
     
    +// Smooth the edges of text (nonzero) or stop; text is smoothed only
    +// when the screen has more than 256 colors.
    +void SysSetTextSmoothing(int32_t on) {
    +    dvxSetFontSmoothing(shellContext(), on != 0);
    +}
    +
    +
     // Change the screen mode.  True when it changed (False keeps the old
     // mode, also while a program has the screen to itself).
     int32_t SysSetVideoMode(int32_t width, int32_t height, int32_t bpp) {
    @@ -470,6 +502,12 @@ int32_t SysShiftState(void) {
     }
     
     
    +// Whether text is smoothed when the screen allows it (the setting).
    +int32_t SysTextSmoothing(void) {
    +    return shellContext()->fontSmoothing ? BAS_TRUE : BAS_FALSE;
    +}
    +
    +
     // Bits per pixel of video mode index (0 out of range).
     int32_t SysVideoModeBpp(int32_t index) {
         const VideoModeInfoT *m = videoMode(index);
    diff --git a/src/libs/kpunch/fileops/fileops.dhs b/src/libs/kpunch/fileops/fileops.dhs
    index a183b246..05d403d1 100644
    --- a/src/libs/kpunch/fileops/fileops.dhs
    +++ b/src/libs/kpunch/fileops/fileops.dhs
    @@ -36,7 +36,7 @@
     
     .h1 File Operations Library
     
    -Attributes, folder listing, path helpers and recursive copy/move/delete jobs with progress, Cancel and conflict handling. The File Manager and Setup are user interfaces over this library; any program can use it. Drives, free space and wildcard filters come from the platform layer (dvxPlat.h): platformDriveList, platformDiskSpace and platformFilterMatch.
    +Attributes, folder listing, path helpers, and recursive copy/move/delete jobs with progress, Cancel, and conflict handling. The File Manager and Setup are user interfaces over this library; any program can use it. Drives, free space, and wildcard filters come from the platform layer (dvxPlat.h): platformDriveList, platformDiskSpace, and platformFilterMatch.
     
     Header: fileops/fileOps.h
     
    @@ -48,7 +48,7 @@ On DOS, touching a drive with no disk (or a write-protected or removed one) rais
     
     .h2 Big Disks and Long File Names
     
    -Disks past 2 GB report their real size and free space: platformDiskSpace asks DOS with the FAT32 call first and falls back to the older one, which stops near 2 GB. File sizes are 64-bit (FileOpsEntryT.size, fileOpsTreeSize, the job byte counts); a FAT32 file reaches 4 GB - 1 byte, and platformStatSize reads it correctly where stat's st_size would be negative. Where a long-name driver is running (Windows, DOSLFN) and covers the drive (platformLongNames), listings show long names, jobs copy and move them, attributes and renames work on them, fileOpsValidateName accepts them and fileOpsPathNormalize keeps their case; elsewhere the 8.3 rules apply.
    +Disks past 2 GB report their real size and free space: platformDiskSpace asks DOS with the FAT32 call first and falls back to the older one, which stops near 2 GB. File sizes are 64-bit (FileOpsEntryT.size, fileOpsTreeSize, the job byte counts); a FAT32 file reaches 4 GB - 1 byte, and platformStatSize reads it correctly where stat's st_size would be negative. Where a long-name driver is running (Windows, DOSLFN) and covers the drive (platformLongNames), listings show long names, jobs copy and move them, attributes and renames work on them, fileOpsValidateName accepts them, and fileOpsPathNormalize keeps their case; elsewhere the 8.3 rules apply.
     
     .h2 Attributes and Dates
     
    @@ -60,12 +60,12 @@ Disks past 2 GB report their real size and free space: platformDiskSpace asks DO
       fileOpsSetTime(path, mtime)                  Set a file's modification time.
     .endtable
     
    -.h2 Listing, Names and Paths
    +.h2 Listing, Names, and Paths
     
     .table
       Function                                              Description
       --------                                              -----------
    -  fileOpsListDir(dir, filter, &entries, &count)         Read a folder into an FileOpsEntryT array (name, size, mtime, isDir). Files not matching filter (platformFilterMatch: wildcards * and ?, several patterns separated by ';', ',' or blanks; "*.*" and an empty filter match everything) are left out; folders always stay. Free with fileOpsListFree.
    +  fileOpsListDir(dir, filter, &entries, &count)         Read a folder into an FileOpsEntryT array (name, size, mtime, isDir). Files not matching filter (platformFilterMatch: wildcards * and ?, several patterns separated by ';', ',', or blanks; "*.*" and an empty filter match everything) are left out; folders always stay. Free with fileOpsListFree.
       fileOpsListSort(entries, count, col, descending)      Folders first, then by FileOpsSortNameE/SizeE/TypeE/DateE; ties by name.
       fileOpsExtension(name)                                Extension without the dot, "" when none.
       fileOpsTypeName(entry, out, size)                     "Folder", "Program" (.APP), "TXT File", "File".
    @@ -78,7 +78,7 @@ Disks past 2 GB report their real size and free space: platformDiskSpace asks DO
       fileOpsValidateName(dir, name, err, errSize)          Check a new name for folder dir by the rules of its disk (platformValidateFilename): on DOS long names where the drive takes them (platformLongNames), else 8.3; never a device name.
       fileOpsPathJoin(out, size, dir, name)                 dir + separator + name in native separators (platformPathJoin: no doubled separator; an absolute name stands alone); false when it does not fit.
       fileOpsPathParent(path, out, size)                    Parent folder, in native separators; false at a root.
    -  fileOpsPathIsRoot(path)                               True for "C:\", "C:" or "/".
    +  fileOpsPathIsRoot(path)                               True for "C:\", "C:", or "/".
       fileOpsPathIsInside(path, dir)                        True when path is dir or below it (platformPathIsUnder).
       fileOpsPathSame(a, b)                                 Same place (platformPathSame: separators equal, case ignored, a trailing separator ignored).
       fileOpsPathNormalize(path, out, size)                 Canonical absolute form of an existing folder (DOS: upper case, except names on a disk with long names, which keep their case).
    @@ -86,7 +86,7 @@ Disks past 2 GB report their real size and free space: platformDiskSpace asks DO
     
     .h2 Jobs
     
    -A job copies, moves or deletes any number of files and folders (recursively):
    +A job copies, moves, or deletes any number of files and folders (recursively):
     
     .code
     FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, "C:\\BACKUP");
    @@ -120,8 +120,8 @@ Each fileOpsJobStep does one bounded piece of work: read one folder, copy one FI
       Step result              Meaning
       -----------              -------
       FileOpsStepBusyE         More work to do.
    -  FileOpsStepAskE          A destination file exists, or a file to delete is read-only. Answer Yes, No, All or Cancel.
    -  FileOpsStepErrorE        Something failed; fileOpsJobMessage says why. Answer Retry, Skip or Cancel.
    +  FileOpsStepAskE          A destination file exists, or a file to delete is read-only. Answer Yes, No, All, or Cancel.
    +  FileOpsStepErrorE        Something failed; fileOpsJobMessage says why. Answer Retry, Skip, or Cancel.
       FileOpsStepDoneE         Finished.
       FileOpsStepCancelledE    Stopped by Cancel.
     .endtable
    @@ -143,6 +143,6 @@ fileOpsJobRun(job, callbacks) runs a job to the end instead: progress(job, ctx)
     
     .h2 BASIC
     
    -src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsPathNative$ (platformPathNative), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename for a name in a folder), FileOpsValidLongName and FileOpsValidShortName (platformValidateLongName / platformValidateShortName, the rules alone), FileOpsLongNames (platformLongNames), FileOpsShortPath$ (platformShortPath, "" when it does not fit), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$) and jobs (FileOpsJobCreate, FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value.
    +src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes, and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsPathNative$ (platformPathNative), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename for a name in a folder), FileOpsValidLongName and FileOpsValidShortName (platformValidateLongName / platformValidateShortName, the rules alone), FileOpsLongNames (platformLongNames), FileOpsShortPath$ (platformShortPath, "" when it does not fit), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$), and jobs (FileOpsJobCreate, FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value.
     
    -Listings and jobs are handles (small numbers, 0 = none) into a table owned by the library; each records the app that made it, and the shell's app-cleanup hook frees what an app leaves open when it ends. FileOpsJobRunFor(job, ms) steps the job until the time is used or it needs an answer, so a BASIC Timer can run it without blocking the program. A returned string stays valid until the next call returning one (the BASIC runtime copies it at once). Single files are handled by the built-in FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR and SETATTR statements.
    +Listings and jobs are handles (small numbers, 0 = none) into a table owned by the library; each records the app that made it, and the shell's app-cleanup hook frees what an app leaves open when it ends. FileOpsJobRunFor(job, ms) steps the job until the time is used or it needs an answer, so a BASIC Timer can run it without blocking the program. A returned string stays valid until the next call returning one (the BASIC runtime copies it at once). Single files are handled by the built-in FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR, and SETATTR statements.
    diff --git a/src/libs/kpunch/game/game.dhs b/src/libs/kpunch/game/game.dhs
    index 415709d6..89ebf1c6 100644
    --- a/src/libs/kpunch/game/game.dhs
    +++ b/src/libs/kpunch/game/game.dhs
    @@ -43,7 +43,7 @@
     
     .h1 Game Helpers Library
     
    -Frame pacing, held keys, program-owned timers and nearest-colour lookup for programs that run like games in a DVX window. It builds on the core's game input windows, held-key table, timer service and high-resolution clock (see Writing Games for DVX).
    +Frame pacing, held keys, program-owned timers, and nearest-colour lookup for programs that run like games in a DVX window. It builds on the core's game input windows, held-key table, timer service, and high-resolution clock (see Writing Games for DVX).
     
     Header: game/game.h
     
    diff --git a/src/libs/kpunch/hlpfile/hlpfile.dhs b/src/libs/kpunch/hlpfile/hlpfile.dhs
    index c224576e..9ed76636 100644
    --- a/src/libs/kpunch/hlpfile/hlpfile.dhs
    +++ b/src/libs/kpunch/hlpfile/hlpfile.dhs
    @@ -37,7 +37,7 @@
     
     .h1 Help File Library
     
    -Reads compiled DVX help files (.hlp, written by the help compiler dvxhlpc or HelpCompile): topics, the table of contents, the keyword index, each topic's content records in order, pictures and full-text search. The Help Viewer, the HelpView widget (BASIC HelpPane) and BASIC's hlpfile.bas all read help files through it, so they find, search and show topics the same way.
    +Reads compiled DVX help files (.hlp, written by the help compiler dvxhlpc or HelpCompile): topics, the table of contents, the keyword index, each topic's content records in order, pictures, and full-text search. The Help Viewer, the HelpView widget (BASIC HelpPane), and BASIC's hlpfile.bas all read help files through it, so they find, search, and show topics the same way.
     
     Header: hlpfile/hlpFile.h (the file format: hlpfile/hlpFormat.h, shared with the help compiler)
     
    @@ -50,7 +50,7 @@ A file is an opaque HlpFileT. Strings returned point into the file's string tabl
     .table
       Function                                      Description
       --------                                      -----------
    -  hlpFileOpen(path)                             Open a help file; NULL when it cannot be read or is not a help file. The header, topic directory, strings, table of contents, index and search index are read at once; topic content and pictures are read when asked for.
    +  hlpFileOpen(path)                             Open a help file; NULL when it cannot be read or is not a help file. The header, topic directory, strings, table of contents, index, and search index are read at once; topic content and pictures are read when asked for.
       hlpFileClose(f)                               Close it (NULL is ignored).
       hlpFileTopicCount(f)                          Number of topics.
       hlpFileTopicId(f, topic) / Title(f, topic)    A topic's ID (what links and HelpTopic name) and title; "" out of range.
    @@ -63,7 +63,7 @@ A file is an opaque HlpFileT. Strings returned point into the file's string tabl
     
     .h2 Records and Pictures
     
    -hlpFileForEachRecord(f, topic, fn, userData) calls fn(hdr, payload, userData) for each record of a topic in order (HLP_REC_TEXT, HEADING1 .. 3, LIST_ITEM, NOTE with its HLP_NOTE_ kind in hdr->flags, CODE, TABLE, IMAGE with its HLP_IMG_ alignment, LINK, HRULE); the payload is NUL-terminated. fn returns false to stop. hlpRecordLink gives a link record's target topic ID and display text (the target when the text is empty); hlpRecordImage its picture's place in the file, and hlpFileReadImage the picture itself: the image file as the compiler took it in (BMP, PNG, JPEG or GIF), malloc'd, for dvxLoadImageAlpha. Reading a picture inside the record walk is safe.
    +hlpFileForEachRecord(f, topic, fn, userData) calls fn(hdr, payload, userData) for each record of a topic in order (HLP_REC_TEXT, HEADING1 .. 3, LIST_ITEM, NOTE with its HLP_NOTE_ kind in hdr->flags, CODE, TABLE, IMAGE with its HLP_IMG_ alignment, LINK, HRULE); the payload is NUL-terminated. fn returns false to stop. hlpRecordLink gives a link record's target topic ID and display text (the target when the text is empty); hlpRecordImage its picture's place in the file, and hlpFileReadImage the picture itself: the image file as the compiler took it in (BMP, PNG, JPEG, or GIF), malloc'd, for dvxLoadImageAlpha. Reading a picture inside the record walk is safe.
     
     hlpWrapText(text, cols) wraps flowing text at cols columns (at least HLP_MIN_WRAP_COLS) as the viewer does, a malloc'd copy with '\n' at the wrap points.
     
    @@ -92,6 +92,6 @@ free(matches);
     
     .h2 BASIC
     
    -src/include/basic/hlpfile.bas declares the BASIC functions (hlpFileBas.c): HlpOpen, HlpClose, HlpTopicCount, HlpTopicId$, HlpTopicTitle$, HlpFindTopic, HlpStartTopic, HlpTocCount, HlpTocTitle$, HlpTocTopic, HlpTocDepth, HlpTocExpanded, HlpIndexCount, HlpIndexKeyword$, HlpIndexTopic, HlpSearch, HlpSearchTopic, HlpLoadTopic, HlpRecordType, HlpRecordFlags, HlpRecordText$, HlpRecordLink$, HlpRecordImageSave and HlpWrap$, with the HLP_REC_, HLP_NOTE_ and HLP_IMG_ constants of hlpFormat.h.
    +src/include/basic/hlpfile.bas declares the BASIC functions (hlpFileBas.c): HlpOpen, HlpClose, HlpTopicCount, HlpTopicId$, HlpTopicTitle$, HlpFindTopic, HlpStartTopic, HlpTocCount, HlpTocTitle$, HlpTocTopic, HlpTocDepth, HlpTocExpanded, HlpIndexCount, HlpIndexKeyword$, HlpIndexTopic, HlpSearch, HlpSearchTopic, HlpLoadTopic, HlpRecordType, HlpRecordFlags, HlpRecordText$, HlpRecordLink$, HlpRecordImageSave, and HlpWrap$, with the HLP_REC_, HLP_NOTE_, and HLP_IMG_ constants of hlpFormat.h.
     
     A handle (a small number, 0 = none) is an open file plus what the program last asked of it -- HlpSearch keeps the topics found for HlpSearchTopic, HlpLoadTopic copies a topic's records for the HlpRecord functions -- since BASIC cannot take a callback or a byte array. Each handle records the program that opened it; the shell's app-cleanup hook closes what a program leaves open when it ends. hlpFileBasHandle(handle) gives the HlpFileT behind a handle to C code (the HelpPane control's ShowTopic uses it). A returned string stays valid until the next call returning one (the BASIC runtime copies it at once).
    diff --git a/src/libs/kpunch/joystick/joystick.dhs b/src/libs/kpunch/joystick/joystick.dhs
    index 98567e9b..a3daef7a 100644
    --- a/src/libs/kpunch/joystick/joystick.dhs
    +++ b/src/libs/kpunch/joystick/joystick.dhs
    @@ -91,11 +91,11 @@ if (joyCount() > 0) {
     }
     .endcode
     
    -joyCount() is the number of devices (the first call of any joystick function finds them); joyGetInfo(device, &info) describes one: name, id (the driver's name and unit number, "gameport0", which prefixes the device's DVX.INI keys), axisCount, buttonCount, hatCount, axisPresent (bit n set when axis n answers; a missing axis reads 0) and caps (JOY_CAP_CALIBRATE for analog axes, JOY_CAP_KEYBOARD for the keyboard joystick).
    +joyCount() is the number of devices (the first call of any joystick function finds them); joyGetInfo(device, &info) describes one: name, id (the driver's name and unit number, "gameport0", which prefixes the device's DVX.INI keys), axisCount, buttonCount, hatCount, axisPresent (bit n set when axis n answers; a missing axis reads 0), and caps (JOY_CAP_CALIBRATE for analog axes, JOY_CAP_KEYBOARD for the keyboard joystick).
     
     joyOpen(ctx, device) returns a handle owned by ctx->currentAppId, or -1. joyOpenShared(ctx, device) returns the calling program's shared handle on the device, opening it the first time (for code with no place to keep a handle; the BASIC functions use it). joyClose closes a handle; a program's handles are closed for it when it ends.
     
    -joyRead(handle, &st) fills a JoyStateT: axes[] (JOY_AXIS_MIN..JOY_AXIS_MAX, 0 inside the dead zone), hats[] (JOY_HAT_CENTERED or degrees clockwise from up), buttons (bit n = button n) and connected (false when the device stopped answering; everything then reads neutral). A button that went down and up again since the handle last looked reads as down once, so a quick tap between two reads is not lost. joyButtonPressed(handle, button) asks the same about one button. joyButtonWentDown(handle, button) only asks whether the button went down since the last time this function asked about it (a press counter of its own, apart from joyRead's).
    +joyRead(handle, &st) fills a JoyStateT: axes[] (JOY_AXIS_MIN..JOY_AXIS_MAX, 0 inside the dead zone), hats[] (JOY_HAT_CENTERED or degrees clockwise from up), buttons (bit n = button n), and connected (false when the device stopped answering; everything then reads neutral). A button that went down and up again since the handle last looked reads as down once, so a quick tap between two reads is not lost. joyButtonPressed(handle, button) asks the same about one button. joyButtonWentDown(handle, button) only asks whether the button went down since the last time this function asked about it (a press counter of its own, apart from joyRead's).
     
     The keyboard joystick (JOY_CAP_KEYBOARD) reads neutral for a handle whose program has no window with the focus, so keys typed into another program do not steer the game.
     
    @@ -132,7 +132,7 @@ joyRescan() looks for devices again (a joystick plugged in later, a changed game
     
     .h2 The Game Port Driver
     
    -joyport.drv drives the standard PC game port at I/O port 201h. Writing to the port starts four one-shot timers, one per axis; each runs for a time proportional to its axis's resistance (about 24 us plus 11 us per kilohm). The driver times them against the platform's microsecond clock (platformMicros) with interrupts left enabled, so the port never holds up the timer, the sound card or the mouse: each sample reads the port and the clock, and an axis whose fall can only be placed in a window wider than JOYPORT_GAP_US (40 us; an interrupt ran in between) keeps its previous value for that poll. A measurement gives up after JOYPORT_TIMEOUT_US (3 ms). At the probe an axis that does not run out, or runs out in under 10 us (some cards read 0 for an unconnected axis), is missing. At run time an axis that times out three polls in a row is left out of the measurement, so an unplugged stick does not cost the timeout every poll, and is tried again every 60 polls; a stick all of whose axes time out reads as not connected. Buttons A1, A2, B1, B2 are read at every poll and sampled by the timer interrupt, so short presses are latched.
    +joyport.drv drives the standard PC game port at I/O port 201h. Writing to the port starts four one-shot timers, one per axis; each runs for a time proportional to its axis's resistance (about 24 us plus 11 us per kilohm). The driver times them against the platform's microsecond clock (platformMicros) with interrupts left enabled, so the port never holds up the timer, the sound card, or the mouse: each sample reads the port and the clock, and an axis whose fall can only be placed in a window wider than JOYPORT_GAP_US (40 us; an interrupt ran in between) keeps its previous value for that poll. A measurement gives up after JOYPORT_TIMEOUT_US (3 ms). At the probe an axis that does not run out, or runs out in under 10 us (some cards read 0 for an unconnected axis), is missing. At run time an axis that times out three polls in a row is left out of the measurement, so an unplugged stick does not cost the timeout every poll, and is tried again every 60 polls; a stick all of whose axes time out reads as not connected. Buttons A1, A2, B1, B2 are read at every poll and sampled by the timer interrupt, so short presses are latched.
     
     In the default mode the port makes up to two joysticks (A with axes 0-1 and buttons 0-1, B with axes 2-3 and buttons 2-3; either may be missing); in the four-axis mode one joystick with four axes and four buttons.
     
    @@ -142,7 +142,7 @@ joykeys.drv is a fallback driver: a joystick made of keys (two axes: full deflec
     
     .h2 Writing a Driver
     
    -A driver is a driver module (see dvxDrv.h in the API reference): a file .drv in bin/libs/kpunch/joystick/drivers, built by the joystick Makefile from each drivers// folder, that exports one JoyDriverT named joyDriver, written with JOY_DRIVER(name, priority, .flags = ..., .probe = ..., .info = ..., .poll = ..., .tick = ..., .settingsChanged = ...). Nothing lists drivers and nothing loads them at startup: the first time a program asks for devices (the GUI and the timer service are running by then), and at every joyRescan, the library loads them all, probes them from the highest priority down (JOY_PRIORITY_PORT, ..., JOY_PRIORITY_KEYBOARD), probes the fallback drivers only when the fallback setting asks for them, and unloads every driver that found no device. The descriptor carries DVX_ABI_VERSION; a driver built for another version, one without probe, info or poll, or one whose name a higher priority driver already has is skipped and logged.
    +A driver is a driver module (see dvxDrv.h in the API reference): a file .drv in bin/libs/kpunch/joystick/drivers, built by the joystick Makefile from each drivers// folder, that exports one JoyDriverT named joyDriver, written with JOY_DRIVER(name, priority, .flags = ..., .probe = ..., .info = ..., .poll = ..., .tick = ..., .settingsChanged = ...). Nothing lists drivers and nothing loads them at startup: the first time a program asks for devices (the GUI and the timer service are running by then), and at every joyRescan, the library loads them all, probes them from the highest priority down (JOY_PRIORITY_PORT, ..., JOY_PRIORITY_KEYBOARD), probes the fallback drivers only when the fallback setting asks for them, and unloads every driver that found no device. The descriptor carries DVX_ABI_VERSION; a driver built for another version, one without probe, info, or poll, or one whose name a higher priority driver already has is skipped and logged.
     
     .table
       Member            Meaning
    @@ -152,7 +152,7 @@ A driver is a driver module (see dvxDrv.h in the API reference): a file .d
       flags             JOY_DRIVER_FALLBACK for a stand-in like the keyboard joystick.
       probe()           Find the units; return how many. Called right after the driver is loaded.
       info(unit, out)   Describe one unit (name, counts, axisPresent, caps); the library fills in id.
    -  poll(raw, units)  Read every unit now, in program context: raw[0..units-1]. Set axisValid for each axis measured (the others keep their last value), buttons held now, pressed for buttons that went down since the last poll, hats (JOY_HAT_CENTERED when not pushed) and connected. Axes are raw units with JOY_CAP_CALIBRATE, else JOY_AXIS_MIN..JOY_AXIS_MAX.
    +  poll(raw, units)  Read every unit now, in program context: raw[0..units-1]. Set axisValid for each axis measured (the others keep their last value), buttons held now, pressed for buttons that went down since the last poll, hats (JOY_HAT_CENTERED when not pushed), and connected. Axes are raw units with JOY_CAP_CALIBRATE, else JOY_AXIS_MIN..JOY_AXIS_MAX.
       tick()            NULL, or called INSIDE THE TIMER INTERRUPT at the poll rate while a handle is open. The timer service's rules apply; the driver pins the code and data it touches with platformLockRegion and releases them with platformUnlockRegion when its module is unloaded (a destructor), so rescans do not pile up locked memory.
       settingsChanged() NULL, or re-read the driver's settings (joySettingGet) after they changed.
     .endtable
    @@ -161,8 +161,8 @@ Up to JOY_MAX_DRIVERS drivers in use get their tick called. A driver is loaded a
     
     .h2 BASIC
     
    -src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton and JoyHat. Joystick, axis, button and hat numbers start at 1; each function reads through the calling program's shared handle.
    +src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton, and JoyHat. Joystick, axis, button, and hat numbers start at 1; each function reads through the calling program's shared handle.
     
    -For a settings program (the Control Panel's Game Controllers page) it also declares: JoyHats, JoyAxisPresent(joy, axis), JoyCanCalibrate(joy) (JoyInfoT hatCount, axisPresent, JOY_CAP_CALIBRATE); JoyConnected(joy) (joyRead's connected); JoyRescan (joyRescanApp for the calling program: its own handles and STICK readings go first; -1 while another program has a joystick open); JoyRawAxis(joy, axis) and JoyRawButtons(joy) (joyGetRaw); JoySetCalibration(joy, axis, minV, centerV, maxV) and JoyClearCalibration(joy, axis) (joySetCalibration); JoyDeadZone / JoySetDeadZone (joyGetDeadZone / joySetDeadZone); JoyChoiceCount(which), JoyChoiceLabel$(which, index), JoyChoice(which), JoySetChoice(which, index) with JOY_CHOICE_GAMEPORT = 0 and JOY_CHOICE_FALLBACK = 1 (JoyChoiceE; joyChoices / joyChoiceGet / joyChoiceSet); JoyKeyCount and JoyKeyName$(index); JoyKbdSlotCount, JoyKbdSlot(slot), JoySetKbdSlot(slot, keyIndex) (joyKbdSlotGet / Set); JoySetting$(key, defaultValue) and JoySetSetting(key, value) ("" removes; joySettingGet / Set); JoySettingsLoad(prefs) and JoySettingsStore(prefs) with a prefs.bas handle (prefsBasHandle); JoyCalibration(joy, axis, part) (joyGetCalibration; part JOY_CAL_MIN / JOY_CAL_CENTER / JOY_CAL_MAX, joystick.h; 0 when joyGetCalibration fails); JoyIsKeyboard(joy) (JOY_CAP_KEYBOARD); JoyKeyIndex(keyName), JoyKeyCode(index), JoyKeyNameOf$(rawCode) (joyKeyIndex / joyKeyCode / joyKeyNameOf). CONSTs JOY_DEADZONE_DEFAULT / _MAX, JOY_POLL_HZ_DEFAULT / _MIN / _MAX and JOY_KEY_POLL_HZ mirror joystick.h. Lists (choices, keys, slots) are numbered from 0; True is -1.
    +For a settings program (the Control Panel's Game Controllers page) it also declares: JoyHats, JoyAxisPresent(joy, axis), JoyCanCalibrate(joy) (JoyInfoT hatCount, axisPresent, JOY_CAP_CALIBRATE); JoyConnected(joy) (joyRead's connected); JoyRescan (joyRescanApp for the calling program: its own handles and STICK readings go first; -1 while another program has a joystick open); JoyRawAxis(joy, axis) and JoyRawButtons(joy) (joyGetRaw); JoySetCalibration(joy, axis, minV, centerV, maxV) and JoyClearCalibration(joy, axis) (joySetCalibration); JoyDeadZone / JoySetDeadZone (joyGetDeadZone / joySetDeadZone); JoyChoiceCount(which), JoyChoiceLabel$(which, index), JoyChoice(which), JoySetChoice(which, index) with JOY_CHOICE_GAMEPORT = 0 and JOY_CHOICE_FALLBACK = 1 (JoyChoiceE; joyChoices / joyChoiceGet / joyChoiceSet); JoyKeyCount and JoyKeyName$(index); JoyKbdSlotCount, JoyKbdSlot(slot), JoySetKbdSlot(slot, keyIndex) (joyKbdSlotGet / Set); JoySetting$(key, defaultValue) and JoySetSetting(key, value) ("" removes; joySettingGet / Set); JoySettingsLoad(prefs) and JoySettingsStore(prefs) with a prefs.bas handle (prefsBasHandle); JoyCalibration(joy, axis, part) (joyGetCalibration; part JOY_CAL_MIN / JOY_CAL_CENTER / JOY_CAL_MAX, joystick.h; 0 when joyGetCalibration fails); JoyIsKeyboard(joy) (JOY_CAP_KEYBOARD); JoyKeyIndex(keyName), JoyKeyCode(index), JoyKeyNameOf$(rawCode) (joyKeyIndex / joyKeyCode / joyKeyNameOf). CONSTs JOY_DEADZONE_DEFAULT / _MAX, JOY_POLL_HZ_DEFAULT / _MIN / _MAX, and JOY_KEY_POLL_HZ mirror joystick.h. Lists (choices, keys, slots) are numbered from 0; True is -1.
     
     The BASIC runtime calls joyBasStick(n) and joyBasStrig(n) for QBasic's STICK and STRIG. Joystick A is device 0 (axes 0-1, buttons 0 and 1); joystick B is device 1, or axes 2-3 and buttons 2-3 of device 0 when it is the only device and has more than two axes. STICK(0) reads A's X and remembers A's Y and B's X and Y for STICK(1) to STICK(3) (per program, forgotten when it ends); positions are 1 to 200 ((value - JOY_AXIS_MIN) x 199 / 65535 + 1), 0 without that joystick. Even STRIG numbers use joyButtonWentDown (0 A button 1, 2 B button 1, 4 A button 2, 6 B button 2), odd ones the state joyRead reports; True is -1.
    diff --git a/src/libs/kpunch/libdvx/Makefile b/src/libs/kpunch/libdvx/Makefile
    index 0bc3171d..adcbba3a 100644
    --- a/src/libs/kpunch/libdvx/Makefile
    +++ b/src/libs/kpunch/libdvx/Makefile
    @@ -36,7 +36,7 @@ LIBSDIR = ../../../../bin/libs
     # Core sources.  dvxPrefs.c is not among them on DOS: the loader needs it
     # before libdvx loads, so dvx.exe links it and exports prefs* to every DXE.
     SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxImage.c dvxImageWrite.c dvxDriver.c \
    -       dvxApp.c dvxDialog.c dvxPrefsBas.c dvxResource.c \
    +       dvxApp.c dvxDialog.c dvxPrefsBas.c dvxResource.c dvxText.c dvxTextBas.c dvxFontFmt.c dvxFlow.c \
            widgetClass.c widgetCore.c widgetScrollbar.c \
            widgetLayout.c widgetEvent.c widgetOps.c widgetIface.c
     
    diff --git a/src/libs/kpunch/libdvx/apiref.dhs b/src/libs/kpunch/libdvx/apiref.dhs
    index fcbec5c2..2d4ed14e 100644
    --- a/src/libs/kpunch/libdvx/apiref.dhs
    +++ b/src/libs/kpunch/libdvx/apiref.dhs
    @@ -40,6 +40,8 @@ The DVX GUI is built as a five-layer architecture. Each layer is defined in its
     .item dvxTypes.h -- Shared type definitions and constants
     .item dvxCur.h -- Embedded mouse cursor bitmaps
     .item dvxFont.h -- Embedded 8x16 CP437 bitmap font
    +.item dvxText.h -- Font families and loaded fonts
    +.item dvxFlow.h -- Flowing text: runs in fonts and colors, wrapped and aligned
     .item dvxPal.h -- 8-bit mode 256-color palette
     .item dvxVideo.h -- Layer 1: VESA VBE video backend
     .item dvxDraw.h -- Layer 2: 2D drawing primitives
    @@ -59,6 +61,8 @@ The DVX GUI is built as a five-layer architecture. Each layer is defined in its
     .link api.types dvxTypes.h -- Shared Type Definitions
     .link api.cursor dvxCur.h -- Cursor Definitions
     .link api.font dvxFont.h -- Bitmap Font Data
    +.link api.text dvxText.h -- Fonts
    +.link api.flow dvxFlow.h -- Flowing Text
     .link api.palette dvxPal.h -- 8-bit Palette
     .link api.video dvxVideo.h -- Layer 1: Video Backend
     .link api.draw dvxDraw.h -- Layer 2: Drawing Primitives
    @@ -107,7 +111,7 @@ Central type definitions shared across all five layers of the DVX GUI stack. Eve
     
     .h2 Binary Interface Version
     
    -DVX_ABI_VERSION is one number for the whole binary interface between dvx.exe, libraries (.lib), widgets (.wgt) and apps (.app). Every module records the value it was compiled with, and a module whose value differs from the running system's is refused instead of being allowed to call the wrong functions:
    +DVX_ABI_VERSION is one number for the whole binary interface between dvx.exe, libraries (.lib), widgets (.wgt), and apps (.app). Every module records the value it was compiled with, and a module whose value differs from the running system's is refused instead of being allowed to call the wrong functions:
     
     .table
       Module / table   Where the value is recorded                                    Checked by
    @@ -127,7 +131,7 @@ Every library and widget module writes the stamp once at file scope in its main
     DVX_ABI_STAMP(mylib);
     .endcode
     
    -Bump rule: increment DVX_ABI_VERSION in the same change that alters anything an already compiled module depends on -- the size, member order or member types of a public struct or widget API struct in an SDK header; a WGT_METHOD_* slot or the meaning of a WidgetClassT slot; the parameters, return type or calling contract of a public or exported function (or its removal); the value of a public constant modules compile in. Adding a function, a constant, or a struct no existing struct embeds needs no bump. After a bump, run make -C src/test/abi update to regenerate the committed size table (src/test/abi/abiSizes.txt). The abi host test fails when a public struct changes size without a bump, or when the table was not regenerated.
    +Bump rule: increment DVX_ABI_VERSION in the same change that alters anything an already compiled module depends on -- the size, member order, or member types of a public struct or widget API struct in an SDK header; a WGT_METHOD_* slot or the meaning of a WidgetClassT slot; the parameters, return type, or calling contract of a public or exported function (or its removal); the value of a public constant modules compile in. Adding a function, a constant, or a struct no existing struct embeds needs no bump. After a bump, run make -C src/test/abi update to regenerate the committed size table (src/test/abi/abiSizes.txt). The abi host test fails when a public struct changes size without a bump, or when the table was not regenerated.
     
     .h2 Core Structures
     
    @@ -200,16 +204,59 @@ Bevel drawing parameters. Swapping highlight/shadow flips raised vs. sunken appe
     
     .h3 BitmapFontT
     
    -Fixed-width 8-pixel-wide bitmap font descriptor. One size provided: 8x16 (standard VGA ROM font, CP437 encoding).
    +The one font type every text call takes. Two kinds share it: the built-in system font (dvxFont8x16: 8-pixel cells of the VGA 8x16 font, one byte per glyph row, glyphs NULL; text takes the byte-aligned fast paths) and a loaded font (dvxFontOpen, dvxText.h: a size of a .DFN font file, or the system font made bold or italic), whose glyphs give each character's advance and bitmap box. Every character of code page 437 draws in either kind.
     
     .table
       Field                          Description
       -----                          -----------
    -  int32_t charWidth              Fixed width per glyph (always 8)
    -  int32_t charHeight             Glyph height (16 in the bundled font)
    -  int32_t firstChar              ASCII code of first glyph (typically 0)
    -  int32_t numChars               Number of glyphs (typically 256)
    -  const uint8_t *glyphData       Packed 1bpp data, charHeight bytes per glyph
    +  int32_t charWidth              Cell width of a fixed-pitch font; the digit 0's width in a proportional one (for "so many characters wide")
    +  int32_t charHeight             Line height
    +  int32_t firstChar              Code of the first glyph (0)
    +  int32_t numChars               Number of glyphs (256)
    +  const uint8_t *glyphData       System font: charHeight bytes per glyph; loaded font: the glyph bitmaps
    +  const FontGlyphT *glyphs       Loaded font: each character's glyph; NULL for the system font
    +  const uint32_t *glyphAt        Loaded font: where each glyph's bitmap starts in glyphData
    +  int32_t ascent                 Rows from the top of a line to the baseline
    +  int32_t pixelSize              The size the font was made at (16 for the system font)
    +  uint32_t style                 FONT_STYLE_BOLD / FONT_STYLE_ITALIC the font draws
    +  uint32_t flags                 FONT_FLAG_PROPORTIONAL, FONT_FLAG_SYNTH_BOLD, FONT_FLAG_SYNTH_ITALIC, FONT_FLAG_GRAY
    +  int32_t underlineY             Row of an underline and of accelerator marks, from the top of a line
    +  int32_t strikeY                Row of a strike-through line
    +  int32_t lineThick              Rows in those lines
    +  void *priv                     The font cache's (dvxFontClose); NULL for fonts not from dvxFontOpen
    +  uint32_t serial                A loaded font's own number, never reused; new when its glyphs are built again (dvxFontSetSmooth). 0 for the system font as compiled in
    +.endtable
    +
    +Measure text with textWidth / textWidthN, never strlen * charWidth: a loaded font may be proportional.
    +
    +.h3 FontGlyphT
    +
    +One character of a loaded font, the same 6-byte record a .DFN file stores. The glyph's bitmap is height rows of FONT_ROW_BYTES(width) bytes, the leftmost pixel in the top bit, at glyphData + glyphAt[character]. In a smoothed font (FONT_FLAG_GRAY) each pixel is FONT_GRAY_BPP bits instead: rows of FONT_ROW_BYTES_BPP(width, FONT_GRAY_BPP) bytes, a pixel's level 0 (none) to FONT_GRAY_FULL read with FONT_GRAY_LEVEL(row, x); FONT_BPP(font) is a font's bits per pixel.
    +
    +.table
    +  Field                          Description
    +  -----                          -----------
    +  uint8_t advance                Pen movement after the glyph
    +  uint8_t width, height          Bitmap size
    +  int8_t left                    Pen position to the bitmap's left column (negative: the glyph reaches back)
    +  int8_t top                     Baseline to the bitmap's top row (up is positive)
    +  uint8_t flags                  FONT_GLYPH_MISSING in a file (a loaded font has every glyph)
    +.endtable
    +
    +.h3 Font styles and flags
    +
    +.table
    +  Define                   Value   Description
    +  ------                   -----   -----------
    +  FONT_STYLE_BOLD          0x01    Bold face (dvxFontOpen)
    +  FONT_STYLE_ITALIC        0x02    Italic face (dvxFontOpen)
    +  FONT_STYLE_UNDERLINE     0x04    Underline drawn with the text (drawTextStyled)
    +  FONT_STYLE_STRIKE        0x08    Strike-through drawn with the text (drawTextStyled)
    +  FONT_STYLE_FACE          0x03    The face bits (bold and italic)
    +  FONT_FLAG_PROPORTIONAL   0x01    Characters differ in width
    +  FONT_FLAG_SYNTH_BOLD     0x02    Bold made from the regular face (thickened)
    +  FONT_FLAG_SYNTH_ITALIC   0x04    Italic made by slanting the upright face
    +  FONT_FLAG_GRAY           0x08    Smoothed: glyph pixels of FONT_GRAY_BPP bits (dvxFontSetSmooth)
     .endtable
     
     .h3 ColorSchemeT
    @@ -300,7 +347,7 @@ Callbacks:
       onRawKey(WindowT *win, const RawKeyEventT *evt)                      Key pressed or released (gameInput windows)
     .endtable
     
    -Game input: while a window with gameInput set has the keyboard focus, DVX keeps only Alt+Tab, Ctrl+Esc and Alt+F4 for itself. Every other keystroke, F1, F10, Alt+letter, Ctrl+F12, accelerators and hotkeys included, goes straight to onKey (except while a menu opened with the mouse, or the keyboard move/size mode, is taking keys). onRawKey then receives each key going down and up (RawKeyEventT: set-1 scancode, extended flag, repeat flag, modifiers from the held-key table), after the frame's onKey calls. When the focus moves away the window gets a release for every key still held (with no modifiers), and the held-key table starts over for the next window. onKeyUp keeps working as for any window. Mouse input is unchanged. The game library (game.h) adds gameKeyIsDown, frame pacing and timers.
    +Game input: while a window with gameInput set has the keyboard focus, DVX keeps only Alt+Tab, Ctrl+Esc, and Alt+F4 for itself. Every other keystroke, F1, F10, Alt+letter, Ctrl+F12, accelerators, and hotkeys included, goes straight to onKey (except while a menu opened with the mouse, or the keyboard move/size mode, is taking keys). onRawKey then receives each key going down and up (RawKeyEventT: set-1 scancode, extended flag, repeat flag, modifiers from the held-key table), after the frame's onKey calls. When the focus moves away the window gets a release for every key still held (with no modifiers), and the held-key table starts over for the next window. onKeyUp keeps working as for any window. Mouse input is unchanged. The game library (game.h) adds gameKeyIsDown, frame pacing, and timers.
     
     .h3 WindowStackT
     
    @@ -494,7 +541,7 @@ KEY_SCAN_* are the raw BIOS scancodes (without KEY_EXT_FLAG): KEY_SCAN_ESC, KEY_
     
     .h3 Raw Key Codes
     
    -A raw key code names one physical key: its set-1 make code (1-127), plus KEY_RAW_EXT (0x80) for keys sent behind an 0xE0 prefix (grey arrows and navigation keys, right Ctrl and Alt, keypad Enter). KEY_RAW_CODE(scancode, extended) builds one. The code is the same whatever modifier is held; letters, digits and punctuation use their KEY_SCAN_* / BIOS scancode. Used by the held-key table (platformKeyIsDown, gameKeyIsDown) and RawKeyEventT.
    +A raw key code names one physical key: its set-1 make code (1-127), plus KEY_RAW_EXT (0x80) for keys sent behind an 0xE0 prefix (grey arrows and navigation keys, right Ctrl and Alt, keypad Enter). KEY_RAW_CODE(scancode, extended) builds one. The code is the same whatever modifier is held; letters, digits, and punctuation use their KEY_SCAN_* / BIOS scancode. Used by the held-key table (platformKeyIsDown, gameKeyIsDown) and RawKeyEventT.
     
     .table
       Define                          Value   Key
    @@ -636,8 +683,262 @@ const BitmapFontT struct describing the 8x16 font, ready to pass to drawText/dra
       FONT_NUM_CHARS      256     Glyph count (full 8-bit character range)
       FONT_CHAR_WIDTH     8       Fixed glyph width in pixels (defined in dvxTypes.h)
       FONT_CHAR_HEIGHT    16      Glyph height in pixels (defined in dvxTypes.h)
    +  FONT_SYS_ASCENT     12      Rows above the baseline (letters sit on row 11)
    +  FONT_SYS_UNDERLINE_Y 15     Underline and accelerator mark row (the cell's last)
    +  FONT_SYS_STRIKE_Y   8       Strike-through row (the middle of the lower-case letters)
    +  FONT_SYS_LINE_THICK 1       Rows in those lines
     .endtable
     
    +.topic api.text
    +.title dvxText.h -- Fonts
    +.toc 1 dvxText.h -- Fonts
    +.index dvxText.h
    +.index dvxFontOpen
    +.index dvxFontClose
    +.index dvxFontFamilyCount
    +.index dvxFontFamilyName
    +.index dvxFontFamilyOf
    +.index dvxFontFamilyFixed
    +.index dvxFontSizes
    +.index dvxFontRescan
    +.index dvxFontRetain
    +.index dvxFontSetSmooth
    +.index dvxFontSmooth
    +.index dvxFontAddFile
    +.index dvxFontAddMemory
    +.index dvxFontAddResources
    +.index dvxFontRemoveOwner
    +.index dvxFontLicence
    +.index Font smoothing
    +.index Font families
    +.index Font licences
    +.index Program fonts
    +.index DFN
    +
    +.h1 dvxText.h -- Fonts
    +
    +The font families DVX draws with: "System", the built-in 8x16 font, and the families in the FONTS folder. Which families that folder holds depends on the install -- Setup puts in none, the base set (DejaVu Sans, Sans Mono, and Serif), or all of them -- and dvxFontOpen of a family that is not there gives the system font. A family has up to four faces (regular, bold, italic, bold italic), each a .DFN file holding the face at the pixel sizes it was made at; the files are made on the build host from TrueType, Type 1, and bitmap fonts (src/tools/mkfont.c), so DVX only copies bitmaps.
    +
    +dvxFontOpen gives a BitmapFontT every text call draws and measures with. It never scales: it picks the face's size nearest the one asked for. A style the family has no face for is made from the nearest face -- bold by thickening each stroke a column (two from 24 pixels), italic by slanting a column per four rows -- and the font's flags say so. Every character of code page 437 draws: what a face lacks is filled in when the font loads. Box drawing, block, and shade characters are drawn to fill the character cell (the pitch, or the digit 0's width) so lines join, also in place of a face's own when its file asks; other characters come from FONT_FALLBACK_FAMILY (FONT_FALLBACK_FIXED for a fixed-pitch face, centered in its pitch) at the nearest size and the same style, or from the system font when that family is not installed.
    +
    +A face can also hold a size smoothed (anti-aliased): each pixel one of four levels, from none to full, drawn as that blend of the text color and what is under it. When smoothing is on (dvxFontSetSmooth) such a size opens smoothed, with FONT_FLAG_GRAY set and FONT_GRAY_BPP bits per pixel; other sizes and faces stay 1-bit. At 8 bits per pixel there are no colors to blend: the pixels of FONT_GRAY_HALF and more are drawn, the rest not.
    +
    +Fonts are shared and counted: the same family, size, and style opened again is the same font, and dvxFontClose gives one up. A few fonts no one holds stay loaded so reopening them is quick. A font's memory belongs to DVX, not to the program that opened it. The family list is read from DVX_FONTS_DIR the first time it is needed; a file that cannot be read is logged and skipped.
    +
    +.h2 Fonts a Program Carries
    +
    +A program can carry fonts of its own: font resources in its file (DVX_RES_FONT, dvxRes.h; a .res line "name font FILE.DFN", or a BASIC project's Fonts), .DFN files it adds with dvxFontAddFile, or fonts in memory (dvxFontAddMemory). The shell adds a program's font resources when it loads the program and forgets them when the program ends; whatever a program adds itself goes then too. Its fonts are its own: every call here works for the running program (the app id the shell sets while a program's code runs, window callbacks included), and other programs never see them.
    +
    +The program sees the installed families and its own. Where it has a family that is installed too, its faces and sizes come first, and the installed family fills in the styles and sizes it lacks: a program that carries DejaVu Sans Bold at 36 pixels gets that size from its own file and every other size and style of DejaVu Sans from the FONTS folder. The nearest size is chosen from both together. Fonts built for a program that carries fonts are cached for it alone; programs that carry none share every font.
    +
    +A .DFN carries its notice (where the font came from and its copyright notices) and its licence, so they go wherever the font goes; dvxFontLicence reads them, and the Font dialog shows them (its Licence button).
    +
    +.h2 Constants
    +
    +.table
    +  Define                  Value                 Description
    +  ------                  -----                 -----------
    +  DVX_FONTS_DIR           "FONTS"               The folder of .DFN files (beside CONFIG)
    +  FONT_SYSTEM_FAMILY      "System"              The built-in 8x16 font's family name
    +  FONT_FALLBACK_FAMILY    "DejaVu Sans"         Fills what a proportional face lacks
    +  FONT_FALLBACK_FIXED     "DejaVu Sans Mono"    Fills what a fixed-pitch face lacks
    +.endtable
    +
    +.h2 dvxFontOpen
    +
    +.code
    +BitmapFontT *dvxFontOpen(const char *family, int32_t pixelSize, uint32_t style);
    +.endcode
    +
    +The font of family at the size nearest pixelSize in style. NULL, "", and a family there is none of give the system font (16 pixels; plain, it is dvxFont8x16 itself, drawn by the fast fixed paths).
    +
    +.table
    +  Parameter   Description
    +  ---------   -----------
    +  family      Family name (any case)
    +  pixelSize   Size wanted, in pixels
    +  style       FONT_STYLE_BOLD and / or FONT_STYLE_ITALIC (the line styles are ignored)
    +.endtable
    +
    +Returns: The font, or NULL when out of memory. Close it with dvxFontClose.
    +
    +.h2 dvxFontClose
    +
    +.code
    +void dvxFontClose(BitmapFontT *font);
    +.endcode
    +
    +Give up a font dvxFontOpen gave. NULL and fonts not from dvxFontOpen (dvxFont8x16, the application context's font) are ignored.
    +
    +.h2 dvxFontFamilyCount / dvxFontFamilyName
    +
    +.code
    +int32_t     dvxFontFamilyCount(void);
    +const char *dvxFontFamilyName(int32_t index);
    +.endcode
    +
    +The number of families (the system font's included) and family index: "System" first, then the others in alphabetical order. dvxFontFamilyName returns NULL for an index out of range.
    +
    +.h2 dvxFontFamilyOf
    +
    +.code
    +const char *dvxFontFamilyOf(const BitmapFontT *font);
    +.endcode
    +
    +The family a font was opened from; "System" for the system font and for fonts not from dvxFontOpen.
    +
    +.h2 dvxFontFamilyFixed
    +
    +.code
    +bool dvxFontFamilyFixed(const char *family);
    +.endcode
    +
    +True when every face of family is fixed-pitch (the system font is; a family there is none of is not): the families an editor or terminal can use.
    +
    +.h2 dvxFontSizes
    +
    +.code
    +int32_t dvxFontSizes(const char *family, uint32_t style, int32_t *sizes, int32_t max);
    +.endcode
    +
    +The pixel sizes family has in style -- those of the face the style is drawn from -- smallest first, into sizes (at most max).
    +
    +Returns: How many sizes there are (which may be more than max); 0 for a family there is none of.
    +
    +.h2 dvxFontRetain
    +
    +.code
    +BitmapFontT *dvxFontRetain(BitmapFontT *font);
    +.endcode
    +
    +Hold font once more, for keeping a font someone else opened (each hold is given up with dvxFontClose). Returns font; fonts not from dvxFontOpen are returned as they are.
    +
    +.h2 dvxFontRescan
    +
    +.code
    +void dvxFontRescan(void);
    +.endcode
    +
    +Read the FONTS folder again on the next call, after fonts were added or removed. The fonts no one holds are unloaded; open fonts stay valid.
    +
    +.h2 dvxFontSetSmooth / dvxFontSmooth
    +
    +.code
    +void dvxFontSetSmooth(bool smooth);
    +bool dvxFontSmooth(void);
    +.endcode
    +
    +Smooth fonts where their face has the size smoothed, or stop; dvxFontSmooth says which it is (off at first). The fonts open are built again in place: the same BitmapFontT pointers with new glyphs and a new serial, so code that keeps measurements of a font compares its serial too, and text is laid out again. The fonts no one holds are unloaded. Programs call dvxSetFontSmoothing (dvxApp.h) instead, which follows the user's choice and the screen's depth.
    +
    +.h2 dvxFontAddFile / dvxFontAddMemory
    +
    +.code
    +bool dvxFontAddFile(int32_t owner, const char *path);
    +bool dvxFontAddMemory(int32_t owner, const void *data, uint32_t length, const char *name);
    +.endcode
    +
    +Add the .DFN at path, or held in data (length bytes, copied at once; name names it in the log), as a face of its family for program owner (an app id; 0 for every program, as SETUP.EXE adds the fonts it carries). Families added for owner 0 are seen by every program and kept by dvxFontRescan.
    +
    +Returns: True when added; false, logged, when the font cannot be read, calls its family "System", or owner has a face of that family and style already.
    +
    +.h2 dvxFontAddResources
    +
    +.code
    +int32_t dvxFontAddResources(int32_t owner, const char *path);
    +.endcode
    +
    +Add every font resource (DVX_RES_FONT) of the file at path -- a program, a library, or a resource file -- for owner. The shell calls it for each program it loads.
    +
    +Returns: How many fonts were added.
    +
    +.h2 dvxFontRemoveOwner
    +
    +.code
    +void dvxFontRemoveOwner(int32_t owner);
    +.endcode
    +
    +Forget every face added for owner (the shell does when a program ends; for 0, the faces added for every program, the installed ones staying). Its fonts no one holds are unloaded; a held one stays as it is until it is closed.
    +
    +.h2 dvxFontLicence
    +
    +.code
    +char *dvxFontLicence(const BitmapFontT *font);
    +.endcode
    +
    +The texts that go with the face font was built from: its notice -- the family and style, the fonts it was converted from, and their copyright notices -- then a blank line and its licence. ASCII with CR LF line ends; malloc'd (the caller frees it).
    +
    +Returns: The texts; NULL for the system font, and when the font's file cannot be read.
    +
    +.topic api.flow
    +.title dvxFlow.h -- Flowing Text
    +.toc 1 dvxFlow.h -- Flowing Text
    +.index dvxFlow.h
    +.index TextFlowT
    +.index dvxFlowCreate
    +.index dvxFlowAddText
    +.index dvxFlowLayout
    +.index dvxFlowPaint
    +.index dvxFlowPaintSel
    +.index dvxFlowHitTest
    +.index dvxFlowCaret
    +.index dvxFlowLineOf
    +.index Flowing text
    +
    +.h1 dvxFlow.h -- Flowing Text
    +
    +A flow is a document built in order: runs of text, each in a font and a color, line breaks, and paragraph starts, each with an alignment. dvxFlowLayout wraps it to a width into lines; dvxFlowPaint draws them. The RichLabel widget is a flow with calls around it; the RichEdit widget lays out each of its paragraphs as a flow and edits through the offset calls below.
    +
    +Layout is greedy: a word and the spaces after it go on the line when the word fits, else the line ends and the word starts the next one, without its leading spaces. A word wider than a whole line is broken where the line is full. A line is as tall as the tallest font on it -- the largest ascent above one baseline and the deepest descent below it -- and its trailing spaces neither count toward its width (alignment) nor draw (an underline would show them). A word may go on across runs (a style changed inside it, or punctuation in a run of its own); it wraps as one word, and only a word wider than a whole line breaks, where the line is full. Underline and strike-through come with a run's font (dvxText.h).
    +
    +Offsets number the text added from 0, each run's characters after the one before (breaks and paragraphs take none); an offset is the caret before that character. Each line owns the offsets from its start to the next line's start -- the spaces it wrapped at included -- and the last line those to the text's end. Text that is all empty lays out as one empty line in its first run's font, so an empty paragraph keeps a height and a caret place.
    +
    +.h2 Constants
    +
    +.table
    +  Define               Value          Description
    +  ------               -----          -----------
    +  FLOW_ALIGN_LEFT      0              Paragraph alignment
    +  FLOW_ALIGN_CENTER    1
    +  FLOW_ALIGN_RIGHT     2
    +  FLOW_COLOR_DEFAULT   0xFFFFFFFF     A run in the color dvxFlowPaint is given
    +  FLOW_WIDTH_NATURAL   INT32_MAX      dvxFlowLayout's width for the natural size: lines break only at breaks and paragraphs
    +.endtable
    +
    +.h2 Functions
    +
    +.table
    +  Function                                                     Description
    +  --------                                                     -----------
    +  TextFlowT *dvxFlowCreate(void)                               An empty flow (one left-aligned paragraph); NULL out of memory.
    +  void dvxFlowDestroy(flow)                                    Free it and give up the fonts it holds. NULL is ignored.
    +  void dvxFlowClear(flow)                                      Empty it again.
    +  bool dvxFlowAddText(flow, text, len, font, color)            Add len bytes of text (len below 0: up to the NUL) in font and color (0x00RRGGBB or FLOW_COLOR_DEFAULT). The flow holds the font (dvxFontRetain) and copies the text.
    +  bool dvxFlowAddBreak(flow)                                   Start a new line in the same paragraph (two in a row make an empty line, as tall as the font before it).
    +  bool dvxFlowAddParagraph(flow, align)                        Start a new paragraph aligned FLOW_ALIGN_*; it ends the line before it when that has text, and on an empty flow it sets the first paragraph's alignment.
    +  int32_t dvxFlowLayout(flow, width)                           Lay it out width pixels wide; returns its height. The same width again costs nothing.
    +  int32_t dvxFlowHeight(flow) / dvxFlowWidth(flow)             The last layout's height and widest line.
    +  void dvxFlowPaint(flow, d, ops, x, y, defaultFg)             Draw the lines that meet the clip rectangle, top-left at x, y, aligned within the width laid out at (the widest line's for FLOW_WIDTH_NATURAL); FLOW_COLOR_DEFAULT runs in defaultFg (a packed pixel). Transparent.
    +  void dvxFlowPaintSel(flow, d, ops, x, y, defaultFg, selStart, selEnd, selFg, selBg)   dvxFlowPaint with offsets selStart to selEnd selected: their text in selFg on selBg (packed pixels), a line's undrawn trailing spaces too.
    +  int32_t dvxFlowTextLength(flow)                              The characters added (the last offset).
    +  int32_t dvxFlowLineCount(flow)                               The last layout's lines.
    +  int32_t dvxFlowLineOf(flow, offset)                          The line the caret before offset is on (a line's end is the next line's start); -1 with no lines.
    +  int32_t dvxFlowLineStart(flow, line)                         The offset a line starts at; past the last line, the text's length.
    +  void dvxFlowLineBox(flow, line, &y, &h)                      A line's top and height.
    +  void dvxFlowCaret(flow, offset, &x, &y, &h)                  The caret before offset: its x, and its line's top and height (alignment included).
    +  int32_t dvxFlowHitTest(flow, x, y)                           The offset nearest a point: on the line at y (the first or last beyond them), the character boundary nearest x. A line that wrapped at a space ends before it.
    +.endtable
    +
    +.code
    +TextFlowT   *flow = dvxFlowCreate();
    +BitmapFontT *bold = dvxFontOpen("DejaVu Sans", 14, FONT_STYLE_BOLD);
    +
    +dvxFlowAddText(flow, "Warning: ", -1, bold, DVX_RGB(160, 0, 0));
    +dvxFlowAddText(flow, "the disk is almost full.", -1, &ctx->font, FLOW_COLOR_DEFAULT);
    +dvxFontClose(bold);
    +dvxFlowLayout(flow, 200);
    +dvxFlowPaint(flow, d, ops, x, y, fg);
    +.endcode
    +
     .topic api.palette
     .title dvxPal.h -- 8-bit Palette
     .toc 1 dvxPal.h -- 8-bit Palette
    @@ -882,7 +1183,7 @@ All 2D drawing operations: rectangle fills, bitmap blits, text rendering, bevels
     void drawInit(BlitOpsT *ops, const DisplayT *d);
     .endcode
     
    -Populate a BlitOpsT with the correct span functions for the display's pixel depth (1, 2 or 4 bytes per pixel). Must be called once after videoInit(). For any other pixel size the span functions are left NULL so misuse fails loudly instead of drawing with the wrong depth.
    +Populate a BlitOpsT with the correct span functions for the display's pixel depth (1, 2, or 4 bytes per pixel). Must be called once after videoInit(). For any other pixel size the span functions are left NULL so misuse fails loudly instead of drawing with the wrong depth.
     
     .table
       Parameter   Description
    @@ -1004,7 +1305,7 @@ Draw a single character glyph. When opaque is true, the background fills the ent
       opaque      true = fill background, false = transparent
     .endtable
     
    -Returns: Advance width (always charWidth).
    +Returns: Advance width (charWidth for the system font, the glyph's advance for a loaded font).
     
     .h2 drawText
     
    @@ -1073,7 +1374,7 @@ Draw a null-terminated string scale times as large: every glyph pixel becomes a
     int32_t textWidth(const BitmapFontT *font, const char *text);
     .endcode
     
    -Return the pixel width of a null-terminated string (strlen(text) * charWidth).
    +Return the pixel width of a null-terminated string: the sum of its characters' advances (for the system font strlen(text) * charWidth).
     
     .table
       Parameter   Description
    @@ -1084,6 +1385,46 @@ Return the pixel width of a null-terminated string (strlen(text) * charWidth).
     
     Returns: Width in pixels.
     
    +.h2 textWidthN
    +
    +.code
    +int32_t textWidthN(const BitmapFontT *font, const char *text, int32_t count);
    +.endcode
    +
    +The pixel width of the first count characters of text (0 when count is 0 or less): where a caret after them goes, how much of a line fits.
    +
    +.table
    +  Parameter   Description
    +  ---------   -----------
    +  font        Bitmap font
    +  text        Characters (not required to be null-terminated)
    +  count       How many to measure
    +.endtable
    +
    +Returns: Width in pixels.
    +
    +.h2 drawTextStyled
    +
    +.code
    +void drawTextStyled(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t style, uint32_t fg, uint32_t bg, bool opaque);
    +void drawTextStyledN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t count, uint32_t style, uint32_t fg, uint32_t bg, bool opaque);
    +.endcode
    +
    +Draw text (drawText, drawTextN) with the lines style asks for: FONT_STYLE_UNDERLINE at the font's underlineY and FONT_STYLE_STRIKE at its strikeY, lineThick rows, as wide as the text, in fg. Bold and italic are the font's own (dvxFontOpen); those bits of style are ignored here.
    +
    +.table
    +  Parameter   Description
    +  ---------   -----------
    +  d           Display context
    +  ops         Blit operations vtable
    +  font        Bitmap font
    +  x, y        Top-left of the line
    +  text        Text (drawTextStyledN: count characters of it)
    +  style       FONT_STYLE_UNDERLINE and / or FONT_STYLE_STRIKE
    +  fg, bg      Foreground and background packed colors
    +  opaque      true = fill background, false = transparent
    +.endtable
    +
     .h2 accelParse
     
     .code
    @@ -1106,7 +1447,7 @@ Returns: Lowercase accelerator character, or 0 if none.
     void drawTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque);
     .endcode
     
    -Draw text with & accelerator markers. The character after & is drawn underlined to indicate the keyboard shortcut. && produces a literal &. Used for menu items and button labels.
    +Draw text with & accelerator markers. The character after & is marked with the font's underline (underlineY; the system font's last row) across its advance to indicate the keyboard shortcut. && produces a literal &. Used for menu items and button labels.
     
     .table
       Parameter   Description
    @@ -1126,7 +1467,7 @@ Draw text with & accelerator markers. The character after & is drawn underlined
     int32_t textWidthAccel(const BitmapFontT *font, const char *text);
     .endcode
     
    -Measure text width excluding & markers (so "&File" measures as 4 chars).
    +Measure text width excluding & markers (so "&File" measures as the width of "File").
     
     .table
       Parameter   Description
    @@ -1194,7 +1535,7 @@ Render an entire row of terminal character cells (ch/attr byte pairs) in a singl
     void drawArrowGlyph(DisplayT *d, const BlitOpsT *ops, int32_t cx, int32_t cy, int32_t rows, ScrollArrowDirE dir, uint32_t color);
     .endcode
     
    -Draw a solid triangle of rows rows (base 2 * rows - 1 pixels) pointing in direction dir (ScrollArrowUpE/DownE/LeftE/RightE), centered on (cx, cy). The shared direction glyph for scrollbar arrows, spinners, tab scroll buttons, navigation bars and sort indicators.
    +Draw a solid triangle of rows rows (base 2 * rows - 1 pixels) pointing in direction dir (ScrollArrowUpE/DownE/LeftE/RightE), centered on (cx, cy). The shared direction glyph for scrollbar arrows, spinners, tab scroll buttons, navigation bars, and sort indicators.
     
     .table
       Parameter   Description
    @@ -1213,7 +1554,7 @@ Draw a solid triangle of rows rows (base 2 * rows - 1 pixels) pointing in direct
     void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, ScrollbarOrientE orient, int32_t x, int32_t y, int32_t length, int32_t barW, int32_t thumbPos, int32_t thumbSize);
     .endcode
     
    -Paint a complete scrollbar from a ready-made thumb geometry: the sunken trough, an arrow button (drawScrollbarArrowButton) at each end, and a raised thumb thumbPos pixels past the first arrow button. The window scrollbars, the widget scrollbars and wmDrawScrollbarAt (which computes the thumb from a scroll state) all paint through it.
    +Paint a complete scrollbar from a ready-made thumb geometry: the sunken trough, an arrow button (drawScrollbarArrowButton) at each end, and a raised thumb thumbPos pixels past the first arrow button. The window scrollbars, the widget scrollbars, and wmDrawScrollbarAt (which computes the thumb from a scroll state) all paint through it.
     
     .table
       Parameter   Description
    @@ -1825,7 +2166,7 @@ Enable or disable a menu item by command ID.
     void wmMenuItemSetLabel(MenuItemT *item, const char *label);
     .endcode
     
    -Give an item a new label, written as wmAddMenuItem takes it: & before the accelerator letter, and the key text after a tab ("&Open\tCtrl+O"). The accelerator follows the new label; the item's ID, type and state stay. An open popup shows the new label the next time it is drawn. A NULL item or label does nothing.
    +Give an item a new label, written as wmAddMenuItem takes it: & before the accelerator letter, and the key text after a tab ("&Open\tCtrl+O"). The accelerator follows the new label; the item's ID, type, and state stay. An open popup shows the new label the next time it is drawn. A NULL item or label does nothing.
     
     .h3 wmAddSubMenu
     
    @@ -2429,6 +2770,9 @@ Install an icon that is already in display pixel format (for example from dvxLoa
     .index dvxMouseToPrefs
     .index dvxWallpaperFromPrefs
     .index dvxWallpaperToPrefs
    +.index dvxSetFontSmoothing
    +.index dvxFontsFromPrefs
    +.index dvxFontsToPrefs
     .index dvxCreateAccelTable
     .index dvxFreeAccelTable
     .index dvxAddAccel
    @@ -2513,11 +2857,12 @@ Fields applications and the shell commonly read (all others are internal state):
       uint32_t windowsCreated      Windows created by dvxCreateWindow so far (the shell compares it before and after a close request to see whether the app showed a prompt)
       int32_t currentAppId         App charged for allocations and callbacks (0 = shell)
       bool busy                    Busy cursor shown and input blocked
    +  bool fontSmoothing           Text smoothing wanted (dvxSetFontSmoothing; on at start)
       idleCallback / idleCtx       Called instead of yielding when a frame had no events
       onCtrlEsc, onF1, onTitleChange   Shell hooks (with their ctx pointers)
       VideoModeInfoT *videoModes   stb_ds array of settable modes
       char wallpaperPath[]         Current wallpaper source path (empty = none)
    -  WallpaperModeE wallpaperMode Stretch, tile or center
    +  WallpaperModeE wallpaperMode Stretch, tile, or center
     .endtable
     
     dvxSetMouseConfig programs the mouse speed and acceleration into the platform layer, which re-applies them after every mouse driver reset; mouseAccel and mouseMickeys only record the last values applied (for dvxMouseToPrefs and settings pages).
    @@ -2699,7 +3044,7 @@ Raise a window to the top of the Z-order and give it focus.
     void dvxRequestWindowClose(AppContextT *ctx, WindowT *win);
     .endcode
     
    -Ask a window to close exactly as its close box, Alt+F4 and the system menu's Close do. The window's onClose runs (under the window's app ID) and decides -- a save prompt may keep the window open; a window without onClose is destroyed. A clean frame is composited first so an open menu is erased before onClose shows a dialog. When no dispatch is in progress the window may already be freed on return, so look it up again by its id rather than using the pointer. The shell's Exit DVX sequence uses this to ask each app to close.
    +Ask a window to close exactly as its close box, Alt+F4, and the system menu's Close do. The window's onClose runs (under the window's app ID) and decides -- a save prompt may keep the window open; a window without onClose is destroyed. A clean frame is composited first so an open menu is erased before onClose shows a dialog. When no dispatch is in progress the window may already be freed on return, so look it up again by its id rather than using the pointer. The shell's Exit DVX sequence uses this to ask each app to close.
     
     .table
       Parameter   Description
    @@ -3104,7 +3449,7 @@ Reset all colors to the built-in defaults and repaint.
     bool dvxLoadTheme(AppContextT *ctx, const char *filename);
     .endcode
     
    -Load a theme file (INI format with [colors] section) and apply it through dvxColorsFromPrefs. Each color is a "name = r,g,b" key in [colors]; keys outside that section, unknown names and malformed values are ignored, and channels are clamped to 0..255. A missing file returns false.
    +Load a theme file (INI format with [colors] section) and apply it through dvxColorsFromPrefs. Each color is a "name = r,g,b" key in [colors]; keys outside that section, unknown names, and malformed values are ignored, and channels are clamped to 0..255. A missing file returns false.
     
     .table
       Parameter   Description
    @@ -3139,7 +3484,7 @@ bool dvxColorsFromPrefs(AppContextT *ctx, PrefsHandleT *h);
     void dvxColorsToPrefs(const AppContextT *ctx, PrefsHandleT *h);
     .endcode
     
    -The [colors] section of an INI handle, the one place its keys are read and written: dvxLoadTheme / dvxSaveTheme, the shell at start (DVX.INI) and Control Panel (OK, and its Cancel snapshot in a prefsCreate handle) all use these. FromPrefs reads every "name = r,g,b" key (malformed ones ignored, channels clamped to 0..255), repaints only when a color changed and returns true when any color key was read. ToPrefs writes every color.
    +The [colors] section of an INI handle, the one place its keys are read and written: dvxLoadTheme / dvxSaveTheme, the shell at start (DVX.INI), and Control Panel (OK, and its Cancel snapshot in a prefsCreate handle) all use these. FromPrefs reads every "name = r,g,b" key (malformed ones ignored, channels clamped to 0..255), repaints only when a color changed and returns true when any color key was read. ToPrefs writes every color.
     
     .h3 dvxColorName
     
    @@ -3216,6 +3561,25 @@ void dvxWallpaperToPrefs(const AppContextT *ctx, PrefsHandleT *h);
     
     The [desktop] wallpaper and mode keys of an INI handle (mode through prefsWallpaperChoices). FromPrefs applies them: a missing mode is WALLPAPER_MODE_DEFAULT and a missing wallpaper means none; the image is built again only when the path or mode differs from the current one. It returns false when the named image could not be loaded. ToPrefs writes the path (removes the key when there is none) and the mode.
     
    +.h2 Text Smoothing
    +
    +.h3 dvxSetFontSmoothing
    +
    +.code
    +void dvxSetFontSmoothing(AppContextT *ctx, bool on);
    +.endcode
    +
    +Smooth text, or stop (ctx->fontSmoothing; on after dvxInit). Text is smoothed when on and the screen has more than 8 bits per pixel (dvxFontSetSmooth, dvxText.h), in the faces and sizes that have a smoothed strike; dvxChangeVideoMode applies it again at the new depth. When what is drawn changes, every window is laid out and painted again.
    +
    +.h3 dvxFontsFromPrefs / dvxFontsToPrefs
    +
    +.code
    +void dvxFontsFromPrefs(AppContextT *ctx, PrefsHandleT *h);
    +void dvxFontsToPrefs(const AppContextT *ctx, PrefsHandleT *h);
    +.endcode
    +
    +The [fonts] keys of an INI handle: smoothing (true or false; true when missing). FromPrefs applies it with dvxSetFontSmoothing; ToPrefs writes the setting. The shell uses FromPrefs at start and Control Panel both for OK and for its Cancel snapshot.
    +
     .h2 Mouse Configuration
     
     .h3 dvxSetMouseConfig
    @@ -3237,7 +3601,7 @@ Configure mouse behavior. Updates the in-memory AppContextT fields and programs
       wheelStep        Lines to scroll per wheel notch (1-10, default 3)
     .endtable
     
    -The speed, acceleration and wheel settings are re-applied by the platform layer after every mouse driver reset, so they survive a live video-mode change. The values are kept in AppContextT (wheelDirection, dblClickMs, mouseAccel, mouseMickeys, wheelStep).
    +The speed, acceleration, and wheel settings are re-applied by the platform layer after every mouse driver reset, so they survive a live video-mode change. The values are kept in AppContextT (wheelDirection, dblClickMs, mouseAccel, mouseMickeys, wheelStep).
     
     .h3 dvxMouseFromPrefs / dvxMouseToPrefs
     
    @@ -3246,7 +3610,7 @@ void dvxMouseFromPrefs(AppContextT *ctx, PrefsHandleT *h);
     void dvxMouseToPrefs(const AppContextT *ctx, PrefsHandleT *h);
     .endcode
     
    -The [mouse] keys of an INI handle: wheel and acceleration (by name, through prefsWheelDirChoices and prefsAccelChoices), doubleclick (milliseconds), speed (mickeys per 8 pixels) and wheelspeed. FromPrefs applies them with dvxSetMouseConfig, defaults for missing keys (MOUSE_*_DEFAULT); ToPrefs writes the configuration in effect (an acceleration no choice has leaves that key alone). The shell uses FromPrefs at start and Control Panel both for OK and for its Cancel snapshot.
    +The [mouse] keys of an INI handle: wheel and acceleration (by name, through prefsWheelDirChoices and prefsAccelChoices), doubleclick (milliseconds), speed (mickeys per 8 pixels), and wheelspeed. FromPrefs applies them with dvxSetMouseConfig, defaults for missing keys (MOUSE_*_DEFAULT); ToPrefs writes the configuration in effect (an acceleration no choice has leaves that key alone). The shell uses FromPrefs at start and Control Panel both for OK and for its Cancel snapshot.
     
     .h2 Input Entropy
     
    @@ -3258,7 +3622,7 @@ typedef void (*DvxEntropyHookT)(const uint8_t *data, int32_t len);
     void dvxSetInputEntropyHook(DvxEntropyHookT fn);
     .endcode
     
    -Install a sink for input-timing entropy (for example a random-number generator's add-entropy routine). While a hook is set, every keyboard event and every mouse change (movement, button, wheel) is recorded as a 32-bit platformEntropyStamp value followed by the event bytes (scancode, ASCII and shift state; or position, buttons and wheel delta). Records are batched and handed to fn once 64 bytes have accumulated, so the cost per event stays negligible. An idle mouse is not recorded. Changing or clearing the hook discards any pending partial batch. The core library has no link-time dependency on any consumer; the shell resolves one at run time.
    +Install a sink for input-timing entropy (for example a random-number generator's add-entropy routine). While a hook is set, every keyboard event and every mouse change (movement, button, wheel) is recorded as a 32-bit platformEntropyStamp value followed by the event bytes (scancode, ASCII, and shift state; or position, buttons, and wheel delta). Records are batched and handed to fn once 64 bytes have accumulated, so the cost per event stays negligible. An idle mouse is not recorded. Changing or clearing the hook discards any pending partial batch. The core library has no link-time dependency on any consumer; the shell resolves one at run time.
     
     .table
       Parameter   Description
    @@ -3313,7 +3677,7 @@ The function that plays the sounds, set by the sound library when it loads (NULL
     
     .h2 Full-Screen Exclusive Mode
     
    -A window's program takes the whole screen in a video mode of its own (a game at 320x200, say) while every other program keeps running: windows keep painting into their own buffers, and the desktop is drawn again when the program lets go.  The owner's window must have the focus: when it loses it (Alt+Tab, Ctrl+Esc, a dialog, another window coming to the front) the desktop comes back and onChange hears ExclusiveSuspendedE; when the window has the focus again the mode is set again and onChange hears ExclusiveActiveE (draw the whole frame: the screen and the backbuffer start black).  While active every key but Alt+Tab, Ctrl+Esc and Alt+F4 goes to the window as in game input mode, the mouse cursor is not drawn, onMouse gets full-screen coordinates, dvxScreenshot saves the full screen, and dvxChangeVideoMode fails.  Closing the window, the program ending or being ended, and dvxExclusiveEnd give the screen back.  See the Writing Games guide.
    +A window's program takes the whole screen in a video mode of its own (a game at 320x200, say) while every other program keeps running: windows keep painting into their own buffers, and the desktop is drawn again when the program lets go.  The owner's window must have the focus: when it loses it (Alt+Tab, Ctrl+Esc, a dialog, another window coming to the front) the desktop comes back and onChange hears ExclusiveSuspendedE; when the window has the focus again the mode is set again and onChange hears ExclusiveActiveE (draw the whole frame: the screen and the backbuffer start black).  While active every key but Alt+Tab, Ctrl+Esc, and Alt+F4 goes to the window as in game input mode, the mouse cursor is not drawn, onMouse gets full-screen coordinates, dvxScreenshot saves the full screen, and dvxChangeVideoMode fails.  Closing the window, the program ending or being ended, and dvxExclusiveEnd give the screen back.  See the Writing Games guide.
     
     .code
     typedef enum {
    @@ -3341,7 +3705,7 @@ Returns: DVX_EXCL_OK; DVX_EXCL_ERR_ARGS (no such window, or a bad size); DVX_EXC
     void dvxExclusiveEnd(AppContextT *ctx, WindowT *win);
     .endcode
     
    -Give the screen back: the desktop's mode, palette and mouse, and a full repaint.  onChange is not called.  Nothing when win does not have full screen.
    +Give the screen back: the desktop's mode, palette, and mouse, and a full repaint.  onChange is not called.  Nothing when win does not have full screen.
     
     .h3 dvxExclusiveDisplay / dvxExclusiveBlitOps
     
    @@ -3458,7 +3822,7 @@ Remove every shortcut of command cmdId from the table: a menu item that lost its
     void dvxAccelFormat(int32_t key, int32_t modifiers, char *out, int32_t outSize);
     .endcode
     
    -Write a key and its modifiers (given as for dvxAddAccel) as text, the way menus show it after a tab: the modifiers Ctrl, Alt and Shift in that order, each followed by +, then the key -- a letter or digit, F1 to F12, or one of the names Backspace, Del, Down, End, Enter, Esc, Home, Ins, Left, PgDn, PgUp, Right, Space, Tab, Up. Key 0 gives "".
    +Write a key and its modifiers (given as for dvxAddAccel) as text, the way menus show it after a tab: the modifiers Ctrl, Alt, and Shift in that order, each followed by +, then the key -- a letter or digit, F1 to F12, or one of the names Backspace, Del, Down, End, Enter, Esc, Home, Ins, Left, PgDn, PgUp, Right, Space, Tab, Up. Key 0 gives "".
     
     .code
     dvxAccelFormat(KEY_ENTER, ACCEL_ALT, text, sizeof(text));   // "Alt+Enter"
    @@ -3470,13 +3834,13 @@ dvxAccelFormat(KEY_ENTER, ACCEL_ALT, text, sizeof(text));   // "Alt+Enter"
     bool dvxAccelParse(const char *text, int32_t *outKey, int32_t *outMods);
     .endcode
     
    -Read key text as dvxAccelFormat writes it ("F2", "Ctrl+X", "Alt+Enter", "Shift+Del") into a key and ACCEL_* modifiers for dvxAddAccel or dvxRegisterHotkey. Case does not matter, blanks around the parts are ignored, and Delete and Insert are accepted for Del and Ins. Returns false, with *outKey 0, for empty text, an unknown modifier or an unknown key.
    +Read key text as dvxAccelFormat writes it ("F2", "Ctrl+X", "Alt+Enter", "Shift+Del") into a key and ACCEL_* modifiers for dvxAddAccel or dvxRegisterHotkey. Case does not matter, blanks around the parts are ignored, and Delete and Insert are accepted for Del and Ins. Returns false, with *outKey 0, for empty text, an unknown modifier, or an unknown key.
     
     .h2 System-Wide Hotkeys
     
     A hotkey is a keystroke that works whichever window has the focus (or when none has): its callback runs instead of anything the focused window would do with the keys, including its menu keys and accelerators. Program Manager's item shortcut keys are hotkeys. Hotkeys are not checked while a menu or a modal dialog is open, and keys the system handles first (Alt+Tab, Alt+F4, Ctrl+F12, Ctrl+Esc, F1, F10, Alt+Space) never reach them.
     
    -A hotkey belongs to the app that was current when it was registered (ctx->currentAppId, set by the shell whenever app code runs). When an app exits, crashes or is ended from the Task Manager, the shell removes its hotkeys with dvxUnregisterAppHotkeys, so a callback never outlives the code it points into. The callback also runs with currentAppId set to that app.
    +A hotkey belongs to the app that was current when it was registered (ctx->currentAppId, set by the shell whenever app code runs). When an app exits, crashes, or is ended from the Task Manager, the shell removes its hotkeys with dvxUnregisterAppHotkeys, so a callback never outlives the code it points into. The callback also runs with currentAppId set to that app.
     
     .h3 dvxRegisterHotkey
     
    @@ -3602,7 +3966,7 @@ Tile windows vertically (stacked, full width, equal height).
     uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch);
     .endcode
     
    -Load an image file (BMP, PNG, JPEG, GIF) and convert to the display's native pixel format. A file that is no image but a DVX program, widget or library with an icon resource gives that icon (dvxResLoadIconData), so every picture that takes a file name can show a program's icon. Caller must free with dvxFreeImage().
    +Load an image file (BMP, PNG, JPEG, GIF) and convert to the display's native pixel format. A file that is no image but a DVX program, widget, or library with an icon resource gives that icon (dvxResLoadIconData), so every picture that takes a file name can show a program's icon. Caller must free with dvxFreeImage().
     
     .table
       Parameter     Description
    @@ -3850,12 +4214,12 @@ Returns: Pixel buffer, or NULL if not found.
     void *dvxResLoadIconData(const char *dxePath, uint32_t *outSize);
     .endcode
     
    -The icon of a DVX program, widget or library file as the bytes of its image file: its DVX_RES_NAME_ICON resource ("icon32", a program's icon), else DVX_RES_NAME_WIDGET_ICON ("icon24", a widget's toolbox icon). Decode it with dvxLoadImageFromMemory or dvxLoadImageAlpha. Caller must free() the buffer.
    +The icon of a DVX program, widget, or library file as the bytes of its image file: its DVX_RES_NAME_ICON resource ("icon32", a program's icon), else DVX_RES_NAME_WIDGET_ICON ("icon24", a widget's toolbox icon). Decode it with dvxLoadImageFromMemory or dvxLoadImageAlpha. Caller must free() the buffer.
     
     .table
       Parameter     Description
       ---------     -----------
    -  dxePath       Path to the program, widget or library file
    +  dxePath       Path to the program, widget, or library file
       outSize       Output: size of the data in bytes
     .endtable
     
    @@ -3932,7 +4296,7 @@ Returns: 32-bit hash value.
     
     .h1 dvxDrv.h -- Driver Modules
     
    -One model for the hardware drivers of every device class (sound, joysticks, ...). A driver is a module of its own, a file .drv in its class's folder under bin/libs (built like a library, but the loader does not load it). The library of the device class loads its drivers only when a program first needs that kind of device, probes them, keeps the ones it uses and unloads the rest, so drivers for hardware that is not there, or for a device class nobody uses, never stay in memory. Nothing lists drivers: adding one is dropping its file into the folder.
    +One model for the hardware drivers of every device class (sound, joysticks, ...). A driver is a module of its own, a file .drv in its class's folder under bin/libs (built like a library, but the loader does not load it). The library of the device class loads its drivers only when a program first needs that kind of device, probes them, keeps the ones it uses, and unloads the rest, so drivers for hardware that is not there, or for a device class nobody uses, never stay in memory. Nothing lists drivers: adding one is dropping its file into the folder.
     
     Each driver exports one descriptor under a name its class chooses (audioDriver, joyDriver, ...). The descriptor is the class's own struct and starts with a DvxDriverHeaderT, written with DVX_DRIVER_HDR (the classes wrap it in their own macro, such as AUDIO_DRIVER or JOY_DRIVER).
     
    @@ -3956,7 +4320,7 @@ DVX_DRIVER_HDR(type, name, priority) fills the header: { DVX_ABI_VERSION, sizeof
     DvxDriverModT *dvxDriversLoad(const char *dir, const DvxDriverClassT *cls);
     .endcode
     
    -Loads every *.drv in dir (relative to the DVX directory), looks each descriptor up and checks it: the ABI version, the size, a name, and the class's valid function. The valid ones come back as an stb_ds array sorted by priority (highest first, by name on a tie); a module that does not load, has no valid descriptor, or repeats a name a higher priority driver already has is logged to DVX.LOG and unloaded. NULL when there are none.
    +Loads every *.drv in dir (relative to the DVX directory), looks each descriptor up, and checks it: the ABI version, the size, a name, and the class's valid function. The valid ones come back as an stb_ds array sorted by priority (highest first, by name on a tie); a module that does not load, has no valid descriptor, or repeats a name a higher priority driver already has is logged to DVX.LOG and unloaded. NULL when there are none.
     
     .h3 dvxDriverKeep
     
    @@ -3984,7 +4348,7 @@ Unloads one module (nothing when it is not loaded) and clears its handle and des
     
     .h2 Writing a Device Class
     
    -A library that has drivers puts them in drivers// in its source folder; its Makefile sets OBJDIR and DRVDIR and writes $(foreach d,$(DVX_DRIVERS),$(eval $(call DVX_DRIVER_RULE,$d))) (common.mk), which builds each folder into $(DRVDIR)/.drv. The library defines its descriptor struct (starting with DvxDriverHeaderT), a macro that writes it, and a DvxDriverClassT; it calls dvxDriversLoad when it first needs devices, dvxDriverKeep for the drivers it uses and dvxDriversFree for the rest. Driver code resolves the library's functions when it loads, since the libraries are loaded for everyone to use.
    +A library that has drivers puts them in drivers// in its source folder; its Makefile sets OBJDIR and DRVDIR and writes $(foreach d,$(DVX_DRIVERS),$(eval $(call DVX_DRIVER_RULE,$d))) (common.mk), which builds each folder into $(DRVDIR)/.drv. The library defines its descriptor struct (starting with DvxDriverHeaderT), a macro that writes it, and a DvxDriverClassT; it calls dvxDriversLoad when it first needs devices, dvxDriverKeep for the drivers it uses, and dvxDriversFree for the rest. Driver code resolves the library's functions when it loads, since the libraries are loaded for everyone to use.
     
     .topic api.widget
     .title dvxWgt.h -- Widget System
    @@ -3999,6 +4363,10 @@ A library that has drivers puts them in drivers// in its source folder; it
     .index wgtSetText
     .index wgtGetText
     .index wgtSetEnabled
    +.index wgtSetFont
    +.index wgtFont
    +.index wgtFontFor
    +.index WCLASS_FIXED_FONT
     .index wgtSetReadOnly
     .index wgtSetFocused
     .index wgtGetFocused
    @@ -4095,6 +4463,7 @@ Core widget structure. Generic across all widget types; type-specific data lives
       WidgetAlignE align                          Main-axis alignment for children
       int32_t spacing, padding                    Tagged sizes for child spacing and padding
       uint32_t fgColor, bgColor                   Custom colors (0 = use scheme defaults)
    +  BitmapFontT *font                           The widget's own font (wgtSetFont); NULL = its container's (wgtFontFor)
       bool visible, enabled, readOnly             State flags
       bool swallowTab                             Tab key goes to widget, not focus navigation
       bool paintDirty                             Needs repaint (set by wgtInvalidatePaint)
    @@ -4130,7 +4499,7 @@ Universal Callbacks:
       onValidate(WidgetT *w)                             Return false to cancel a write
     .endtable
     
    -The universal callbacks belong to the widget's host (the application or the form runtime). A widget class sees the same input earlier through its own class methods: WGT_METHOD_ON_MOUSE_EVENT, WGT_METHOD_ON_KEY, WGT_METHOD_ON_KEY_UP, WGT_METHOD_ON_FOCUS, WGT_METHOD_ON_BLUR and WGT_METHOD_ON_WHEEL run before the matching callback (see Method IDs).
    +The universal callbacks belong to the widget's host (the application or the form runtime). A widget class sees the same input earlier through its own class methods: WGT_METHOD_ON_MOUSE_EVENT, WGT_METHOD_ON_KEY, WGT_METHOD_ON_KEY_UP, WGT_METHOD_ON_FOCUS, WGT_METHOD_ON_BLUR, and WGT_METHOD_ON_WHEEL run before the matching callback (see Method IDs).
     
     .h2 Size Specification Macros
     
    @@ -4161,7 +4530,8 @@ The universal callbacks belong to the widget's host (the application or the form
       WCLASS_PRESS_RELEASE        Click = press + release (Button, ImageButton)
       WCLASS_ACCEL_WHEN_HIDDEN    Accelerator matching works even when invisible
       WCLASS_HAS_TEXT             The first field of w->data is the widget's own text (strdup / free)
    -  WCLASS_RAISES_EVENTS        The class fires the universal mouse, click, key and scroll callbacks itself (wgtFireEvent); the dispatcher does not call them for its widgets. Its class methods still get every event. A BASIC UserControl has it: its host sees only what the control's code raises. GotFocus / LostFocus still reach onFocus / onBlur.
    +  WCLASS_RAISES_EVENTS        The class fires the universal mouse, click, key, and scroll callbacks itself (wgtFireEvent); the dispatcher does not call them for its widgets. Its class methods still get every event. A BASIC UserControl has it: its host sees only what the control's code raises. GotFocus / LostFocus still reach onFocus / onBlur.
    +  WCLASS_FIXED_FONT           The class edits text by character columns (TextBox, TextArea, Terminal, ComboBox, Spinner): given a proportional font it draws with the system font instead (wgtFontFor); a fixed-pitch font of any size is used.
     .endtable
     
     .h2 Window Integration
    @@ -4260,6 +4630,28 @@ Get the widget's current text content.
     
     Returns: Text string (empty string if no handler).
     
    +.h3 wgtSetFont / wgtFont / wgtFontFor
    +
    +.code
    +bool               wgtSetFont(WidgetT *w, const char *family, int32_t size, uint32_t style);
    +const BitmapFontT *wgtFont(const WidgetT *w);
    +const BitmapFontT *wgtFontFor(const WidgetT *w, const BitmapFontT *inherited);
    +.endcode
    +
    +wgtSetFont gives w the font of family at the size nearest size in style (dvxFontOpen, dvxText.h: FONT_STYLE_BOLD, ITALIC, UNDERLINE, STRIKE); the widgets inside w draw with it too unless they have their own -- a form's font is its root widget's. A NULL family gives the font up, so w uses its container's again. The window is laid out again. Returns false when out of memory (w keeps the font it had). The font is closed when w is destroyed.
    +
    +wgtFontFor is the font w draws with: its own, else the nearest container's up the parent chain, else inherited (the font passed down: the application's, or NULL for a metrics query that needs none). A class with WCLASS_FIXED_FONT given a proportional font gets the system font. The class dispatch helpers (wclsPaint, wclsPaintOverlay, wclsCalcMinSize, wclsLayout, wclsGetLayoutMetrics, wclsGetPopupRect) hand every method this font, so a widget's paint, measure, and layout methods simply use their font parameter. Code that is given no font -- mouse and key handlers, helpers -- calls wgtFont(w), the same with the application's font. Size hints in characters (wgtChars) count the widget's own font.
    +
    +.table
    +  Parameter   Description
    +  ---------   -----------
    +  w           Widget
    +  family      Family name (NULL: inherit again)
    +  size        Pixel size wanted
    +  style       FONT_STYLE_* bits
    +  inherited   The font the caller would otherwise use
    +.endtable
    +
     .h3 wgtSetEnabled
     
     .code
    @@ -4434,7 +4826,7 @@ int32_t wgtGetSpacingPx(const WidgetT *w);
     void    wgtSetSpacingPx(WidgetT *w, int32_t pixels);
     .endcode
     
    -A container's padding (space inside its edge) and spacing (gap between children) in pixels. The getters return what the layout uses: the container's default while the field is WGT_GAP_DEFAULT (every widget starts so), and 0 for a size given in another unit. The setters clamp to 0..WGT_GAP_PIXELS_MAX (0 is no gap) and re-lay out the window. The BASIC Padding and Spacing properties of VBox, HBox, Frame and ScrollPane use these.
    +A container's padding (space inside its edge) and spacing (gap between children) in pixels. The getters return what the layout uses: the container's default while the field is WGT_GAP_DEFAULT (every widget starts so), and 0 for a size given in another unit. The setters clamp to 0..WGT_GAP_PIXELS_MAX (0 is no gap) and re-lay out the window. The BASIC Padding and Spacing properties of VBox, HBox, Frame, and ScrollPane use these.
     
     .h3 wgtLayout
     
    @@ -4542,7 +4934,7 @@ Remove the interface registered under name (wgtRegisterIface). A widget class ma
     void wgtUnregisterAppClasses(int32_t appId);
     .endcode
     
    -Release every class, API (wgtRegisterApi) and interface (wgtRegisterIface) registered while appId was the current app (AppContextT.currentAppId at registration time), since all of them point into the app's module. The shell calls it whenever it unloads an app (normal exit or End Task), once the app's windows and widgets are gone. appId 0 (boot-time widget modules) is ignored.
    +Release every class, API (wgtRegisterApi), and interface (wgtRegisterIface) registered while appId was the current app (AppContextT.currentAppId at registration time), since all of them point into the app's module. The shell calls it whenever it unloads an app (normal exit or End Task), once the app's windows and widgets are gone. appId 0 (boot-time widget modules) is ignored.
     
     .table
       Parameter   Description
    @@ -4778,7 +5170,7 @@ The following inline functions provide type-safe dispatch through the WidgetClas
       wclsScrollChildIntoView(parent, child)  WGT_METHOD_SCROLL_CHILD_INTO_VIEW  Scroll child visible
       wclsOnBlur(w)                           WGT_METHOD_ON_BLUR                 Widget lost keyboard focus
       wclsOnWheel(w, delta)                   WGT_METHOD_ON_WHEEL                Mouse wheel; true = consumed
    -  wclsOnMouseEvent(w, action, button, x, y, shift)  WGT_METHOD_ON_MOUSE_EVENT  Every press, release and move (hover too)
    +  wclsOnMouseEvent(w, action, button, x, y, shift)  WGT_METHOD_ON_MOUSE_EVENT  Every press, release, and move (hover too)
       wclsOnKeyUp(w, keyCode, shift)          WGT_METHOD_ON_KEY_UP               Key released on the focused widget
       wclsOnFocus(w)                          WGT_METHOD_ON_FOCUS                Widget gained keyboard focus
     .endtable
    @@ -4875,7 +5267,7 @@ Describes a BASIC-exposed property on a widget type. Used by the form runtime an
       enumNames   For WGT_IFACE_ENUM: NULL-terminated array of value names
     .endtable
     
    -A property is readable when getFn is not NULL and writable when setFn is not NULL -- the one rule for every interface (the form runtime, the designer and the property sheet all apply it). A dynamic interface (WgtIfaceT.dyn) puts WGT_DYN_ACCESS in getFn / setFn for "present"; they are never called. A FLOAT getter returns float and its setter takes float.
    +A property is readable when getFn is not NULL and writable when setFn is not NULL -- the one rule for every interface (the form runtime, the designer, and the property sheet all apply it). A dynamic interface (WgtIfaceT.dyn) puts WGT_DYN_ACCESS in getFn / setFn for "present"; they are never called. A FLOAT getter returns float and its setter takes float.
     
     .h3 WgtMethodDescT
     
    @@ -4923,7 +5315,7 @@ Calling convention IDs (method sig field). IDs are stable ABI; new ones are appe
       WGT_SIG_DYNAMIC            26   bool fn(WidgetT *, const WgtValueT *args, int32_t argc, WgtValueT *ret) (WgtDynMethodFnT): any arguments, as they come
     .endtable
     
    -wgtSigInfo describes each signature (WgtSigInfoT: argc, retType and argTypes[], WGT_SIG_MAX_ARGS = 5; NULL for WGT_SIG_DYNAMIC). wgtIfaceCallMethod is the one place that calls through these signatures.
    +wgtSigInfo describes each signature (WgtSigInfoT: argc, retType, and argTypes[], WGT_SIG_MAX_ARGS = 5; NULL for WGT_SIG_DYNAMIC). wgtIfaceCallMethod is the one place that calls through these signatures.
     
     .h3 WgtEventDescT
     
    @@ -4989,7 +5381,7 @@ typedef struct {
     } WgtValueT;
     .endcode
     
    -One property value, method argument or result, or event argument. WGT_IFACE_NONE (5) is "no value" (a void method's result). A STRING value never owns its text: an argument's text belongs to the caller for the duration of the call; a returned one (a property read, a method result) belongs to the widget and stays valid until the next call on that widget or until the widget changes or is destroyed -- copy it to keep it. wgtValueInt, wgtValueBool, wgtValueFloat, wgtValueDouble and wgtValueStr (NULL becomes "") build values. FLOAT is BASIC's Single, DOUBLE its Double: a DOUBLE property or argument keeps every digit (a value through FLOAT is rounded to a float by a static widget's getter and setter, and the form runtime shows it as a Single); as text a DOUBLE has 15 significant digits.
    +One property value, method argument or result, or event argument. WGT_IFACE_NONE (5) is "no value" (a void method's result). A STRING value never owns its text: an argument's text belongs to the caller for the duration of the call; a returned one (a property read, a method result) belongs to the widget and stays valid until the next call on that widget or until the widget changes or is destroyed -- copy it to keep it. wgtValueInt, wgtValueBool, wgtValueFloat, wgtValueDouble, and wgtValueStr (NULL becomes "") build values. FLOAT is BASIC's Single, DOUBLE its Double: a DOUBLE property or argument keeps every digit (a value through FLOAT is rounded to a float by a static widget's getter and setter, and the form runtime shows it as a Single); as text a DOUBLE has 15 significant digits.
     
     .h3 wgtValueToInt / wgtValueToBool / wgtValueToFloat / wgtValueToText
     
    @@ -5012,7 +5404,7 @@ The .frm and property-sheet boolean rule: "True" or "-1" is true, "False" or "0"
     
     .h2 Generic Widget Access
     
    -One implementation of "create, read, write and call by name" for every widget, static or dynamic. The form runtime, the IDE designer and C programs all use it; nothing else switches on WGT_SIG_* or WgtCreateSigE.
    +One implementation of "create, read, write, and call by name" for every widget, static or dynamic. The form runtime, the IDE designer, and C programs all use it; nothing else switches on WGT_SIG_* or WgtCreateSigE.
     
     .h3 wgtIfaceSetType / wgtGetIfaceOf
     
    @@ -5105,7 +5497,7 @@ bool wgtFireEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc);
     
     Fire event name on w. The universal events go to their WidgetT callback with their arguments taken from args (a missing one is 0): Click -> onClick, DblClick -> onDblClick, Change -> onChange, GotFocus -> onFocus, LostFocus -> onBlur, KeyPress (KeyAscii) -> onKeyPress, KeyDown / KeyUp (KeyCode, Shift) -> onKeyDown / onKeyUp, MouseDown / MouseUp / MouseMove (Button, X, Y, or the VB form Button, Shift, X, Y) -> onMouseDown / onMouseUp / onMouseMove (which take no Shift), Scroll (Delta) -> onScroll (names case-insensitive). Every other name goes to w->onEvent; the universal names never do, so a host that sets both sees each event once. Validate is not mapped (onValidate answers a question; fire it directly). onEvent may write back into args (ByRef parameters; the form runtime writes every ByRef parameter back, a String as text that stays valid until the next event). Returns false when a handler destroyed widgets -- do not touch w then.
     
    -ListView (ColumnClick), TextArea (GutterClick) and HelpView (LinkClick) fire their own events this way after their C callbacks (wgtListViewSetHeaderClickCallback, wgtTextAreaSetGutterClick, wgtHelpViewSetLinkCallback), which stay for C programs; the event is skipped when the C callback destroyed widgets. A TextArea's gutter takes clicks when either is set.
    +ListView (ColumnClick), TextArea (GutterClick), and HelpView (LinkClick) fire their own events this way after their C callbacks (wgtListViewSetHeaderClickCallback, wgtTextAreaSetGutterClick, wgtHelpViewSetLinkCallback), which stay for C programs; the event is skipped when the C callback destroyed widgets. A TextArea's gutter takes clicks when either is set.
     
     .h2 Property Data Type Constants
     
    @@ -5150,6 +5542,8 @@ WGT_MAX_CREATE_ARGS is 3.
     .index dvxIntInputBox
     .index dvxChoiceDialog
     .index dvxColorDialog
    +.index dvxFontDialog
    +.index DvxFontChoiceT
     .index Color Dialog
     .index dvxDialogCreate
     .index dvxDialogAddOkCancel
    @@ -5411,7 +5805,7 @@ Returns: true if a choice was made, false if cancelled.
     bool dvxColorDialog(AppContextT *ctx, const char *title, uint32_t *rgbInOut);
     .endcode
     
    -Display a modal color picker: a palette of 48 basic colors, Red, Green and Blue sliders each paired with a number box (0-255), and a preview swatch. Clicking a basic color sets all three channels; the basic color matching the current color is framed. From the keyboard, Tab moves between the sliders and number boxes (the Red slider has the focus first), the arrow keys, Page Up/Down, Home and End move a slider, and Enter chooses OK. Works at every color depth; on 8-bit desktops the swatches show the nearest palette colors, but the chosen value is exact.
    +Display a modal color picker: a palette of 48 basic colors, Red, Green, and Blue sliders each paired with a number box (0-255), and a preview swatch. Clicking a basic color sets all three channels; the basic color matching the current color is framed. From the keyboard, Tab moves between the sliders and number boxes (the Red slider has the focus first), the arrow keys, Page Up/Down, Home, and End move a slider, and Enter chooses OK. Works at every color depth; on 8-bit desktops the swatches show the nearest palette colors, but the chosen value is exact.
     
     .table
       Parameter     Description
    @@ -5423,7 +5817,7 @@ Display a modal color picker: a palette of 48 basic colors, Red, Green and Blue
     
     Returns: true if OK was chosen, false if cancelled or closed (then *rgbInOut is unchanged).
     
    -Macros for the 0x00RRGGBB layout: DVX_RGB(r, g, b) builds a color from three 0-255 channels, and DVX_RGB_RED(rgb), DVX_RGB_GREEN(rgb) and DVX_RGB_BLUE(rgb) take one apart.
    +Macros for the 0x00RRGGBB layout: DVX_RGB(r, g, b) builds a color from three 0-255 channels, and DVX_RGB_RED(rgb), DVX_RGB_GREEN(rgb), and DVX_RGB_BLUE(rgb) take one apart.
     
     .code
     uint32_t rgb = DVX_RGB(r, g, b);
    @@ -5447,7 +5841,7 @@ void     dvxDialogEnd(WindowT *dlg, bool accepted);
     bool     dvxDialogRun(AppContextT *ctx, WindowT *dlg);
     .endcode
     
    -dvxDialogCreate makes an empty dialog contentW pixels wide (NULL on failure); add widgets under dlg->widgetRoot, or labelled text boxes with dvxDialogAddField. dvxDialogAddOkCancel adds the right-aligned OK (labelled okLabel, "&OK" when NULL) and Cancel buttons after them. dvxDialogRun fits the height to the widgets, centers the dialog, runs it and destroys it; it returns true when it ended with OK. Every created dialog must be run.
    +dvxDialogCreate makes an empty dialog contentW pixels wide (NULL on failure); add widgets under dlg->widgetRoot, or labelled text boxes with dvxDialogAddField. dvxDialogAddOkCancel adds the right-aligned OK (labelled okLabel, "&OK" when NULL) and Cancel buttons after them. dvxDialogRun fits the height to the widgets, centers the dialog, runs it, and destroys it; it returns true when it ended with OK. Every created dialog must be run.
     
     dvxDialogAddField adds a row: a label labelW pixels wide and a text box of up to maxLen characters holding value, filling the rest of the row. It returns the text box (NULL on failure); the box's parent is the row, so a Browse... button can be added after it with wgtButton(box->parent, ...). Give every field's label the same labelW so the boxes line up.
     
    @@ -5481,6 +5875,35 @@ Return codes for dvxPromptSave:
       DVX_SAVE_CANCEL   3       Abort the operation
     .endtable
     
    +.h3 dvxFontDialog
    +
    +.code
    +bool dvxFontDialog(AppContextT *ctx, const char *title, uint32_t flags, DvxFontChoiceT *choiceInOut);
    +.endcode
    +
    +Display a modal font picker: lists of the font families (dvxText.h), the four styles (Regular, Bold, Italic, and Bold Italic), and the sizes the chosen family comes in for the chosen style; Underline and Strikeout boxes; and a sample drawn in the chosen font as it changes. Choosing another family or style keeps the size nearest the one chosen. The labels' underlined letters move to the lists from the keyboard; Enter chooses OK.
    +
    +.table
    +  Parameter     Description
    +  ---------     -----------
    +  ctx           Application context
    +  title         Window title (NULL: "Font")
    +  flags         DVX_FONT_DLG_FIXED: offer only fixed-pitch families (for a text editor or a terminal)
    +  choiceInOut   In: the starting font (a family there is none of starts on "System"); out: the chosen one
    +.endtable
    +
    +DvxFontChoiceT holds family (FONT_FAMILY_MAX characters with the NUL), size (pixels), and style (FONT_STYLE_* bits, the lines included) -- what wgtSetFont and dvxFontOpen take.
    +
    +Returns: true if OK was chosen, false if cancelled or closed (then *choiceInOut is unchanged).
    +
    +.code
    +DvxFontChoiceT choice = { "DejaVu Sans", 14, FONT_STYLE_BOLD };
    +
    +if (dvxFontDialog(ctx, NULL, 0, &choice)) {
    +    wgtSetFont(label, choice.family, choice.size, choice.style);
    +}
    +.endcode
    +
     .h3 dvxPromptSave
     
     .code
    @@ -5534,6 +5957,7 @@ Reading starts from the end: seek to EOF - sizeof(footer), verify the magic, the
       DVX_RES_ICON        1              Image data (BMP, PNG, etc.)
       DVX_RES_TEXT        2              Null-terminated string
       DVX_RES_BINARY      3              Arbitrary binary data (app-specific)
    +  DVX_RES_FONT        4              A .DFN font the program carries (added for it when it loads; dvxText.h)
       DVX_RES_MAGIC       0x52585644     Footer signature ("DVXR" little-endian)
       DVX_RES_NAME_MAX    32             Max resource name length including null
       DVX_RES_ONLY_MAGIC  "DVXRESONLY"   First bytes (NUL included, DVX_RES_ONLY_MAGIC_LEN) of a file that holds only resources
    @@ -5724,7 +6148,7 @@ Returns: 0 on success, -1 if not found, the name is too long, or I/O failed.
     
     Handle-based API over classic INI files. Each prefsLoad/prefsCreate returns a PrefsHandleT that must be passed to all subsequent calls and freed with prefsClose when done. Multiple INI files can be open simultaneously.
     
    -BASIC programs reach the same functions through prefs.bas (dvxPrefsBas.c in libdvx): PrefsLoad, PrefsCreate, PrefsGet$, PrefsGetInt, PrefsGetBool, PrefsSet, PrefsSetInt, PrefsSetBool, PrefsRemove, PrefsKeyName$ (prefsKeyName, "" past the last key), PrefsSave, PrefsSaveAs and PrefsClose, on handles (1, 2, ...; 0 = none) that belong to the program that made them and are closed, unsaved, when it ends. True is -1, False 0. PrefsDvxIni$ is DVX_INI_PATH, PrefsSetVideoMode is prefsSetVideoMode, and PrefsVideoWidth / PrefsVideoHeight / PrefsVideoBpp are the three values of prefsGetVideoMode (DVX_DEFAULT_VIDEO_* for a missing key or a bad handle). The choice tables below are PREFS_CHOICE_ACCEL (0), PREFS_CHOICE_WALLPAPER (1) and PREFS_CHOICE_WHEEL (2) there: PrefsChoiceCount, PrefsChoiceLabel$, PrefsChoiceName$ (iniName), PrefsChoiceValue, PrefsChoiceIndex, PrefsChoiceIndexOfValue, PrefsChoiceDefault (the index of the choice MOUSE_ACCEL_DEFAULT / WALLPAPER_MODE_DEFAULT / MOUSE_WHEEL_DIR_DEFAULT names; -1 for an unknown table), and PrefsGetChoice(handle, section, key, table) with the table's default. PrefsGetChoiceFrom(handle, section, key, choiceNames, defaultName) is prefsGetChoice on a table the program gives as "|"-separated names (each choice's iniName and label is its name, its value its index).
    +BASIC programs reach the same functions through prefs.bas (dvxPrefsBas.c in libdvx): PrefsLoad, PrefsCreate, PrefsGet$, PrefsGetInt, PrefsGetBool, PrefsSet, PrefsSetInt, PrefsSetBool, PrefsRemove, PrefsKeyName$ (prefsKeyName, "" past the last key), PrefsSave, PrefsSaveAs, and PrefsClose, on handles (1, 2, ...; 0 = none) that belong to the program that made them and are closed, unsaved, when it ends. True is -1, False 0. PrefsDvxIni$ is DVX_INI_PATH, PrefsSetVideoMode is prefsSetVideoMode, and PrefsVideoWidth / PrefsVideoHeight / PrefsVideoBpp are the three values of prefsGetVideoMode (DVX_DEFAULT_VIDEO_* for a missing key or a bad handle). The choice tables below are PREFS_CHOICE_ACCEL (0), PREFS_CHOICE_WALLPAPER (1), and PREFS_CHOICE_WHEEL (2) there: PrefsChoiceCount, PrefsChoiceLabel$, PrefsChoiceName$ (iniName), PrefsChoiceValue, PrefsChoiceIndex, PrefsChoiceIndexOfValue, PrefsChoiceDefault (the index of the choice MOUSE_ACCEL_DEFAULT / WALLPAPER_MODE_DEFAULT / MOUSE_WHEEL_DIR_DEFAULT names; -1 for an unknown table), and PrefsGetChoice(handle, section, key, table) with the table's default. PrefsGetChoiceFrom(handle, section, key, choiceNames, defaultName) is prefsGetChoice on a table the program gives as "|"-separated names (each choice's iniName and label is its name, its value its index).
     
     .code
     PrefsHandleT *prefsBasHandle(int32_t handle);
    @@ -5773,8 +6197,8 @@ File format rules:
     .item Lines of any length are read without truncation; LF and CR LF endings are accepted.
     .item "[name]" starts a section; "key = value" is a key (blanks around key and value are trimmed). Keys before the first section belong to section "".
     .item Section and key names match case-insensitively. With duplicates, the first one wins.
    -.item Blank lines, ';' and '#' comments and lines that are neither a section nor a key are kept.
    -.item On save, every line not changed through the API (comments, unknown lines, untouched keys and headers) keeps its content; changed or new keys are written as "key = value". Every line is written with CR LF endings, so a file with LF-only endings is rewritten in full.
    +.item Blank lines, ';' and '#' comments, and lines that are neither a section nor a key are kept.
    +.item On save, every line not changed through the API (comments, unknown lines, untouched keys, and headers) keeps its content; changed or new keys are written as "key = value". Every line is written with CR LF endings, so a file with LF-only endings is rewritten in full.
     .endlist
     
     .table
    @@ -5878,7 +6302,7 @@ Returns: Parsed int, or defaultVal.
     bool prefsGetBool(PrefsHandleT *h, const char *section, const char *key, bool defaultVal);
     .endcode
     
    -Retrieve a boolean value. The whole value must be "true", "yes" or "1" (true) or "false", "no" or "0" (false), in any case; anything else (e.g. "t", "10", "normal") returns defaultVal.
    +Retrieve a boolean value. The whole value must be "true", "yes", or "1" (true) or "false", "no", or "0" (false), in any case; anything else (e.g. "t", "10", "normal") returns defaultVal.
     
     .table
       Parameter    Description
    @@ -5898,7 +6322,7 @@ void prefsGetVideoMode(PrefsHandleT *h, int32_t *width, int32_t *height, int32_t
     void prefsSetVideoMode(PrefsHandleT *h, int32_t width, int32_t height, int32_t bpp);
     .endcode
     
    -Read or store the [video] display mode DVX starts in (keys width, height and bpp). Missing keys read as DVX_DEFAULT_VIDEO_W/H/BPP (640x480, 16 bpp). The one place those key names live: the shell (start-up and safe video mode), the Control Panel and Setup all go through these two functions.
    +Read or store the [video] display mode DVX starts in (keys width, height, and bpp). Missing keys read as DVX_DEFAULT_VIDEO_W/H/BPP (640x480, 16 bpp). The one place those key names live: the shell (start-up and safe video mode), the Control Panel, and Setup all go through these two functions.
     
     .h2 Setters
     
    @@ -5950,7 +6374,7 @@ for (int32_t i = 0; (key = prefsKeyName(h, "associations", i)) != NULL; i++) {
     
     .h2 Shared Preference Vocabularies
     
    -Enumerated preferences are stored in DVX.INI by name. One table per preference is the single source of truth for the label the Control Panel shows, the spelling written to the INI and the value applied.
    +Enumerated preferences are stored in DVX.INI by name. One table per preference is the single source of truth for the label the Control Panel shows, the spelling written to the INI, and the value applied.
     
     .code
     typedef struct {
    @@ -5972,9 +6396,9 @@ extern const PrefsChoiceT prefsWheelDirChoices[PREFS_WHEEL_DIR_CHOICE_COUNT];
       prefsWheelDirChoices    [mouse] wheel           normal: 1, reversed: -1 (MOUSE_WHEEL_DIR_*)           MOUSE_WHEEL_DIR_DEFAULT ("normal")
     .endtable
     
    -The entry counts are PREFS_ACCEL_CHOICE_COUNT (4), PREFS_WALLPAPER_CHOICE_COUNT (3) and PREFS_WHEEL_DIR_CHOICE_COUNT (2). MOUSE_WHEEL_DIR_NORMAL (1) and MOUSE_WHEEL_DIR_REVERSED (-1) in dvxTypes.h are also the wheelDir values for dvxSetMouseConfig.
    +The entry counts are PREFS_ACCEL_CHOICE_COUNT (4), PREFS_WALLPAPER_CHOICE_COUNT (3), and PREFS_WHEEL_DIR_CHOICE_COUNT (2). MOUSE_WHEEL_DIR_NORMAL (1) and MOUSE_WHEEL_DIR_REVERSED (-1) in dvxTypes.h are also the wheelDir values for dvxSetMouseConfig.
     
    -Labels are the capitalized names ("Off", "Stretch", "Reversed", ...). The accel table is in dropdown order. dvxMouseFromPrefs and dvxWallpaperFromPrefs (dvxApp.h) read [mouse] wheel, [mouse] acceleration and [desktop] mode through prefsGetChoice, and the Control Panel builds its dropdowns from the same tables.
    +Labels are the capitalized names ("Off", "Stretch", "Reversed", ...). The accel table is in dropdown order. dvxMouseFromPrefs and dvxWallpaperFromPrefs (dvxApp.h) read [mouse] wheel, [mouse] acceleration, and [desktop] mode through prefsGetChoice, and the Control Panel builds its dropdowns from the same tables.
     
     .h3 prefsChoiceIndex
     
    @@ -6128,6 +6552,7 @@ Free every tracked allocation charged to the given app. Called by the shell afte
     .index widgetScrollbarApplyHit
     .index widgetScrollbarNeeds
     .index widgetDrawScrollbarV
    +.index widgetQuickPaintBegin
     
     .h1 dvxWgtP.h -- Widget Plugin API
     
    @@ -6278,7 +6703,7 @@ Find the deepest widget at (x, y) within w's subtree. Respects WCLASS_NO_HIT_REC
     WidgetT *widgetCallbackTarget(WidgetT *root, int32_t x, int32_t y);
     .endcode
     
    -Find the widget the universal callbacks (onClick, onDblClick, onMouseDown/Up/Move), context menus, tooltips and the cursor shape go to. Starts from widgetHitTest; while the result is a non-focusable WCLASS_NO_HIT_RECURSE container (Splitter, ScrollPane), it continues into the child whose paintClip (the part actually visible when last painted) contains the point. Returns the container itself when the point is on its own chrome (divider, scrollbars) or no child is visible there.
    +Find the widget the universal callbacks (onClick, onDblClick, onMouseDown/Up/Move), context menus, tooltips, and the cursor shape go to. Starts from widgetHitTest; while the result is a non-focusable WCLASS_NO_HIT_RECURSE container (Splitter, ScrollPane), it continues into the child whose paintClip (the part actually visible when last painted) contains the point. Returns the container itself when the point is on its own chrome (divider, scrollbars) or no child is visible there.
     
     .h3 widgetClosePopup
     
    @@ -6286,7 +6711,7 @@ Find the widget the universal callbacks (onClick, onDblClick, onMouseDown/Up/Mov
     void widgetClosePopup(void);
     .endcode
     
    -Close the open dropdown/combobox popup, if any: dirty its overlay rect and fully invalidate its window so the overlay pixels are erased. The single close path for click-outside, clicks in another window, Escape and focus changes.
    +Close the open dropdown/combobox popup, if any: dirty its overlay rect and fully invalidate its window so the overlay pixels are erased. The single close path for click-outside, clicks in another window, Escape, and focus changes.
     
     .h3 widgetIsEnabledChain
     
    @@ -6427,7 +6852,7 @@ Map a dragged thumb position to a scroll position in [0, maxScroll]. relMouse is
     void widgetScrollbarNeeds(int32_t totalW, int32_t totalH, int32_t *innerW, int32_t *innerH, bool *needV, bool *needH);
     .endcode
     
    -Which scrollbars content totalW x totalH pixels needs in a viewport of *innerW x *innerH (the area inside the widget's border). A vertical bar narrows the viewport, which may make the content too wide; a horizontal bar shortens it, which may make the content too tall; both cases are followed through. *innerW and *innerH come back less the bars (WGT_SB_W each). For rows of a fixed height pass rows * height: the content is too tall exactly when there are more rows than whole rows fit. ListBox, ListView, TreeView, ScrollPane and DBGrid decide their bars with it; BASIC's ScrollBarsNeeded is the same rule.
    +Which scrollbars content totalW x totalH pixels needs in a viewport of *innerW x *innerH (the area inside the widget's border). A vertical bar narrows the viewport, which may make the content too wide; a horizontal bar shortens it, which may make the content too tall; both cases are followed through. *innerW and *innerH come back less the bars (WGT_SB_W each). For rows of a fixed height pass rows * height: the content is too tall exactly when there are more rows than whole rows fit. ListBox, ListView, TreeView, ScrollPane, and DBGrid decide their bars with it; BASIC's ScrollBarsNeeded is the same rule.
     
     .h2 Layout Functions
     
    @@ -6492,6 +6917,14 @@ void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const
     
     Paint the overlay pass: dropdown popups and tooltips drawn on top of the main tree.
     
    +.h3 widgetQuickPaintBegin
    +
    +.code
    +bool widgetQuickPaintBegin(WidgetT *w, DisplayT *cd);
    +.endcode
    +
    +A display onto w's window content buffer, clipped to w and every ancestor, for a widget's quick repaint (WGT_METHOD_QUICK_REPAINT) outside the paint walk: the widget draws the rows that changed and reports them, and only those are flushed. Returns false when w cannot be painted that way now -- hidden, clipped away entirely, or inside a scroll container, whose viewport is not its rect (w is then invalidated for the normal paint) -- and the caller drops what it meant to draw. The Terminal and RichEdit widgets use it.
    +
     .h2 Pressable Button Helpers
     
     Shared state machine for WCLASS_PRESS_RELEASE widgets (Button, ImageButton). The w->pressed flag tracks the visual state; these helpers fire the widget's onClick callback on release-within-bounds.
    @@ -6669,7 +7102,7 @@ Append a formatted line to dvx.log. Implemented in dvx.exe and exported to every
     void platformInit(void);
     .endcode
     
    -One-time platform initialization (a second call does nothing new; the loader and dvxInit both call it). On DOS this disables Ctrl+C and Ctrl+Break at every level (the DJGPP runtime check, SIGINT, and DOS break checking via INT 21h AH=33h) so the user cannot kill the GUI from inside a graphics mode, and points the real-mode INT 24h (critical error) vector at a stub in DOS memory that answers Fail. From then on a drive that is not ready, a write-protected disk or a removed disk makes the DOS call return an error (errno) instead of waiting at "Abort, Retry, Fail" on the hidden text screen, so no DVX code needs a guard of its own. The handler is never removed: DOS restores the vector from the PSP when DVX exits. It does not install crash handlers (see platformInstallCrashHandler).
    +One-time platform initialization (a second call does nothing new; the loader and dvxInit both call it). On DOS this disables Ctrl+C and Ctrl+Break at every level (the DJGPP runtime check, SIGINT, and DOS break checking via INT 21h AH=33h) so the user cannot kill the GUI from inside a graphics mode, and points the real-mode INT 24h (critical error) vector at a stub in DOS memory that answers Fail. From then on a drive that is not ready, a write-protected disk, or a removed disk makes the DOS call return an error (errno) instead of waiting at "Abort, Retry, Fail" on the hidden text screen, so no DVX code needs a guard of its own. The handler is never removed: DOS restores the vector from the PSP when DVX exits. It does not install crash handlers (see platformInstallCrashHandler).
     
     .h3 platformYield
     
    @@ -6730,17 +7163,17 @@ void    platformTimerRemoveOwner(int32_t ownerAppId);
     void    platformTimerStats(PlatformTimerStatsT *out);
     .endcode
     
    -One timer interrupt for everything that must run at a steady rate however busy the programs are (audio refill, joystick polling). dvxInit starts the service; dvxShutdown stops it. On DOS it owns PIT channel 0 (rate generator mode) and INT 8: the interrupt runs as fast as the fastest registered timer needs (at most PLATFORM_TIMER_MAX_HZ; the BIOS 18.2 Hz when there is none) and exactly one interrupt per 65536 PIT clocks is passed on to the BIOS handler, so the BIOS tick count, the DOS time of day, clock() and the floppy motor timeout keep their normal rate. A rate change takes effect at once (the PIT is restarted and the part of the period already run is counted). Shutdown restores the BIOS square-wave mode and the old vector; it also runs at exit() and from the crash handler when a crash is not recovered.
    +One timer interrupt for everything that must run at a steady rate however busy the programs are (audio refill, joystick polling). dvxInit starts the service; dvxShutdown stops it. On DOS it owns PIT channel 0 (rate generator mode) and INT 8: the interrupt runs as fast as the fastest registered timer needs (at most PLATFORM_TIMER_MAX_HZ; the BIOS 18.2 Hz when there is none) and exactly one interrupt per 65536 PIT clocks is passed on to the BIOS handler, so the BIOS tick count, the DOS time of day, clock(), and the floppy motor timeout keep their normal rate. A rate change takes effect at once (the PIT is restarted and the part of the period already run is counted). Shutdown restores the BIOS square-wave mode and the old vector; it also runs at exit() and from the crash handler when a crash is not recovered.
     
     platformTimerAdd calls fn(userData) hz times a second, 1 to PLATFORM_TIMER_MAX_HZ; exact on average, while single intervals vary by up to one interrupt period. It returns a timer id, or -1 (bad arguments, all PLATFORM_TIMER_SLOTS in use, service not running). ownerAppId is recorded for platformTimerRemoveOwner, which the shell calls when a program ends; use 0 for a library that removes its own timers. gameTimerAdd (game.lib) registers one for the calling program. After platformTimerRemove returns, the callback is not running and will not run again.
     
     platformTimerAddFast is the same for a device that must be fed at an audio rate (the PC speaker's digital sound): hz may be up to PLATFORM_TIMER_FAST_MAX_HZ. The whole timer interrupt then runs that often, so the callback must be very short (a few port writes); ask for PLATFORM_PIT_HZ / divisor to be called on every interrupt.
     
     .note warning
    -The callback runs INSIDE THE TIMER INTERRUPT, with interrupts disabled, on a small stack. Keep it to a few dozen microseconds; do not call the C library, DVX or DOS; do not use floating point; and pin its code and every byte it touches with platformLockRegion (a page fault there is fatal on DOS). Set flags and counters, fill buffers, write I/O ports.
    +The callback runs INSIDE THE TIMER INTERRUPT, with interrupts disabled, on a small stack. Keep it to a few dozen microseconds; do not call the C library, DVX, or DOS; do not use floating point; and pin its code and every byte it touches with platformLockRegion (a page fault there is fatal on DOS). Set flags and counters, fill buffers, write I/O ports.
     .endnote
     
    -PlatformTimerStatsT reports rateHz (the interrupt rate, rounded), divisor (the PIT reload value), interrupts (since the service started) and biosTicks (of those, passed on to the BIOS).
    +PlatformTimerStatsT reports rateHz (the interrupt rate, rounded), divisor (the PIT reload value), interrupts (since the service started), and biosTicks (of those, passed on to the BIOS).
     
     .h3 Interrupt-Safe Updates and Shutdown Hooks
     
    @@ -6776,7 +7209,7 @@ Low-level video mode setup and teardown. Called by videoInit / videoShutdown.
     void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *userData), void *userData);
     .endcode
     
    -Enumerate the graphics modes platformVideoInit can actually set (on DOS: VBE modes with a linear framebuffer at 8, 15, 16 or 32 bpp; 24 bpp is not offered). cb is invoked once per distinct width/height/depth. Feeds the video-mode list offered to the user; platformVideoInit selects its own best match independently.
    +Enumerate the graphics modes platformVideoInit can actually set (on DOS: VBE modes with a linear framebuffer at 8, 15, 16, or 32 bpp; 24 bpp is not offered). cb is invoked once per distinct width/height/depth. Feeds the video-mode list offered to the user; platformVideoInit selects its own best match independently.
     
     .h3 platformVideoSetPalette
     
    @@ -6834,7 +7267,7 @@ Copy count pixels from src to dst. On DOS these use rep movsd.
     void platformMouseInit(int32_t screenW, int32_t screenH);
     .endcode
     
    -Initialize the mouse driver and constrain movement to the screen bounds. On DOS this resets the INT 33h driver, sets the X/Y range and centers the cursor. Safe to call again after a video-mode change: the speed, acceleration and wheel settings applied earlier are re-applied after the driver reset.
    +Initialize the mouse driver and constrain movement to the screen bounds. On DOS this resets the INT 33h driver, sets the X/Y range, and centers the cursor. Safe to call again after a video-mode change: the speed, acceleration, and wheel settings applied earlier are re-applied after the driver reset.
     
     .h3 platformMousePoll
     
    @@ -6910,7 +7343,7 @@ void platformKeyStateClear(void);
     void platformInjectScan(uint8_t scan);
     .endcode
     
    -The keyboard hook (on DOS a chain on INT 9 that reads port 60h before the BIOS handler stores the key for platformKeyboardRead) decodes every set-1 byte: 0xE0 prefixes, the fake Shift codes the keyboard wraps around grey keys (dropped), the Pause burst (one press and one release) and keyboard replies such as ACK and overrun (dropped). Each key going down or up updates the held-key table and is queued as a RawKeyEventT (at most PLATFORM_KEY_EVENT_QUEUE; newer events are dropped when the queue is full, the table still changes). Keys are named by raw code, KEY_RAW_CODE(scancode, extended) (see dvxTypes.h): Left and Right Shift, Ctrl and Alt are keys of their own, and the code does not change with modifiers. dvxInit installs the hook and dvxShutdown removes it; the event loop reads the queue (onKeyUp, onRawKey) and clears the table whenever the keyboard focus moves. platformKeyStateGet copies the table (1 = down). platformInjectScan feeds one byte to the decoder as if the keyboard had sent it (the test script's KEYDOWN / KEYUP use it).
    +The keyboard hook (on DOS a chain on INT 9 that reads port 60h before the BIOS handler stores the key for platformKeyboardRead) decodes every set-1 byte: 0xE0 prefixes, the fake Shift codes the keyboard wraps around grey keys (dropped), the Pause burst (one press and one release), and keyboard replies such as ACK and overrun (dropped). Each key going down or up updates the held-key table and is queued as a RawKeyEventT (at most PLATFORM_KEY_EVENT_QUEUE; newer events are dropped when the queue is full, the table still changes). Keys are named by raw code, KEY_RAW_CODE(scancode, extended) (see dvxTypes.h): Left and Right Shift, Ctrl, and Alt are keys of their own, and the code does not change with modifiers. dvxInit installs the hook and dvxShutdown removes it; the event loop reads the queue (onKeyUp, onRawKey) and clears the table whenever the keyboard focus moves. platformKeyStateGet copies the table (1 = down). platformInjectScan feeds one byte to the decoder as if the keyboard had sent it (the test script's KEYDOWN / KEYUP use it).
     
     .h3 platformAltScanToChar
     
    @@ -6946,7 +7379,7 @@ Query total and free physical memory.
     const char *platformValidateFilename(const char *dir, const char *name);
     .endcode
     
    -Validate a name for a new file or folder in folder dir (NULL or "" for the current one) against the rules of its disk. On DOS: platformValidateLongName where platformLongNames(dir), else platformValidateShortName, and dir with the name must stay under DVX_MAX_PATH. The host backend refuses an empty name, . and .., separators and control characters, and ignores dir. Returns NULL if valid, or a human-readable error string. The file dialog, the File Manager (fileOpsValidateName) and the IDE's project rename all use it.
    +Validate a name for a new file or folder in folder dir (NULL or "" for the current one) against the rules of its disk. On DOS: platformValidateLongName where platformLongNames(dir), else platformValidateShortName, and dir with the name must stay under DVX_MAX_PATH. The host backend refuses an empty name, . and .., separators, and control characters, and ignores dir. Returns NULL if valid, or a human-readable error string. The file dialog, the File Manager (fileOpsValidateName), and the IDE's project rename all use it.
     
     .h3 Long file names: platformLongNames / platformValidateLongName / platformValidateShortName / platformLongNameChar / platformShortPath
     
    @@ -6960,11 +7393,11 @@ bool        platformLongNameChar(char c);
     bool        platformShortPath(const char *path, char *out, int32_t outSize);
     .endcode
     
    -The DOS C library uses the long-name calls (INT 21h 71xxh) by itself whenever a driver provides them (Windows, DOSLFN), so open, fopen, rename, mkdir, stat and the directory readers all take and return long names; these functions cover what the C library does not.
    +The DOS C library uses the long-name calls (INT 21h 71xxh) by itself whenever a driver provides them (Windows, DOSLFN), so open, fopen, rename, mkdir, stat, and the directory readers all take and return long names; these functions cover what the C library does not.
     
     platformLongNames is true when the drive holding path (NULL or "": the current drive) takes long file names. On DOS that is a loaded long-name driver covering that drive; elsewhere always.
     
    -platformValidateLongName and platformValidateShortName are the two DOS name rules by themselves, whatever the disk takes; each returns NULL or why the name is not valid. A long name has 1 to PLATFORM_LONG_NAME_MAX characters and may hold spaces, several dots and + , ; = [ ]; it may not hold a control character or \ / : * ? " < > |, and may not end with a dot or a space (DOS drops them). An 8.3 name has 1 to 8 printable ASCII characters, then at most one dot and up to 3 more, none of them a space or " * + , / : ; < = > ? [ \ ] |. Neither may be . or .., or a device name (CON, PRN, AUX, NUL, CLOCK$, COM1 to COM9, LPT1 to LPT9) before its first dot. They are shared code, so the host tests check the DOS rules too.
    +platformValidateLongName and platformValidateShortName are the two DOS name rules by themselves, whatever the disk takes; each returns NULL or why the name is not valid. A long name has 1 to PLATFORM_LONG_NAME_MAX characters and may hold spaces, several dots, and + , ; = [ ]; it may not hold a control character or \ / : * ? " < > |, and may not end with a dot or a space (DOS drops them). An 8.3 name has 1 to 8 printable ASCII characters, then at most one dot and up to 3 more, none of them a space or " * + , / : ; < = > ? [ \ ] |. Neither may be . or .., or a device name (CON, PRN, AUX, NUL, CLOCK$, COM1 to COM9, LPT1 to LPT9) before its first dot. They are shared code, so the host tests check the DOS rules too.
     
     platformLongNameChar is true when a long name may hold character c; a file transfer uses it to turn the characters a sender's name cannot keep into _.
     
    @@ -7006,7 +7439,7 @@ bool        platformPathDirName(const char *path, char *out, int32_t outSize);
     char       *platformPathTidy(char *path);
     .endcode
     
    -DVX paths follow DOS rules on every platform: '/' and '\\' are the same separator, so a path a program writes with '/' works on DOS exactly as with '\\', and names compare without case (DOS and the host build also share INI files, projects and program paths). Every path DVX shows uses the native separator, DVX_PATH_SEP ('\\' on DOS). DVX's own path code goes through these functions; a program that splits, compares or joins paths should too, never testing for one separator by hand.
    +DVX paths follow DOS rules on every platform: '/' and '\\' are the same separator, so a path a program writes with '/' works on DOS exactly as with '\\', and names compare without case (DOS and the host build also share INI files, projects, and program paths). Every path DVX shows uses the native separator, DVX_PATH_SEP ('\\' on DOS). DVX's own path code goes through these functions; a program that splits, compares, or joins paths should too, never testing for one separator by hand.
     
     .list
     .item platformPathIsSep: true for '/' and '\\'.
    @@ -7068,7 +7501,7 @@ Write path with its file-name extension replaced by ext into out, followed by as
     bool platformReplaceFile(const char *tmpPath, const char *path, const char *bakPath);
     .endcode
     
    -Crash-safe swap of a fully written tmpPath in for path. The original is renamed aside to bakPath first (DOS rename cannot overwrite) and removed only once the replacement is in place; if the swap fails the original is restored and tmpPath removed. A directory at path is never moved aside: the call fails and removes tmpPath. A leftover bakPath is removed only while path exists; with path missing it may be the only copy left by a crash between the two renames, so it is kept (prefsLoad reads it back). The preferences system, the resource writer and the help compiler all save this way, with the PLATFORM_TMP_EXT and PLATFORM_BAK_EXT siblings from platformPathSibling.
    +Crash-safe swap of a fully written tmpPath in for path. The original is renamed aside to bakPath first (DOS rename cannot overwrite) and removed only once the replacement is in place; if the swap fails the original is restored and tmpPath removed. A directory at path is never moved aside: the call fails and removes tmpPath. A leftover bakPath is removed only while path exists; with path missing it may be the only copy left by a crash between the two renames, so it is kept (prefsLoad reads it back). The preferences system, the resource writer, and the help compiler all save this way, with the PLATFORM_TMP_EXT and PLATFORM_BAK_EXT siblings from platformPathSibling.
     
     .h3 platformReadFile
     
    @@ -7093,7 +7526,7 @@ Simple glob pattern match. Case-insensitive. Supports * (zero or more chars) and
     bool platformFilterMatch(const char *name, const char *filter);
     .endcode
     
    -True when name matches filter: platformGlobMatch patterns separated by ';', ',' or blanks ("*.txt;*.bat", "*.bmp, *.jpg"). A pattern equal to PLATFORM_FILTER_ALL matches every file, with or without an extension, as on DOS; a NULL or blank filter matches everything too. The file dialog (dvxFileDialog) and the file operations library (fileOpsListDir) both use it, so a filter behaves the same everywhere.
    +True when name matches filter: platformGlobMatch patterns separated by ';', ',', or blanks ("*.txt;*.bat", "*.bmp, *.jpg"). A pattern equal to PLATFORM_FILTER_ALL matches every file, with or without an extension, as on DOS; a NULL or blank filter matches everything too. The file dialog (dvxFileDialog) and the file operations library (fileOpsListDir) both use it, so a filter behaves the same everywhere.
     
     .h3 platformDriveList / platformDiskSpace / platformBootDrive
     
    @@ -7181,7 +7614,7 @@ Copy src into dst without its leading spaces and tabs or its trailing whitespace
     const char *dvxNextArg(const char *p, char *out, int32_t outSize, bool *quoted);
     .endcode
     
    -Copy the next argument of the command line p into out and return the text after it, or NULL when only blanks are left (or p is NULL). Arguments are separated by blanks and tabs; one that starts with a double quote runs to the closing quote (or the end), keeps its blanks and loses the quotes, and *quoted is set. The copy is truncated to fit outSize. This is the one rule for splitting command lines: the shell's Run command, program launch arguments and the BASIC COMMAND$ function all use it. Walk every argument with:
    +Copy the next argument of the command line p into out and return the text after it, or NULL when only blanks are left (or p is NULL). Arguments are separated by blanks and tabs; one that starts with a double quote runs to the closing quote (or the end), keeps its blanks, and loses the quotes, and *quoted is set. The copy is truncated to fit outSize. This is the one rule for splitting command lines: the shell's Run command, program launch arguments, and the BASIC COMMAND$ function all use it. Walk every argument with:
     
     .code
     const char *p = args;
    @@ -7267,7 +7700,7 @@ Log platform-specific crash diagnostics. Called internally from the crash handle
     void platformOutOfMemory(size_t bytes);
     .endcode
     
    -Out-of-memory hook for the stb_ds dynamic arrays and hash maps (arrput, hmput, shput and friends have no error return). stb_ds_wrap.h points STBDS_OUT_OF_MEMORY at it wherever the stb_ds implementation is compiled. It logs "out of memory (N bytes) in dynamic array" via dvxLog and raises SIGSEGV, so under platformInstallCrashHandler a failed growth takes the normal crash-recovery path: the shell terminates the app that was current (its task, or its callback running on the shell task), and a failure with no app current is logged and the shell returns to its main loop. With no handler installed (the loader, the tools, the BASIC compiler) the process terminates. It never returns. The stb_ds patch that adds the hook is recorded in thirdparty/stb_ds.PATCHES.md.
    +Out-of-memory hook for the stb_ds dynamic arrays and hash maps (arrput, hmput, shput, and friends have no error return). stb_ds_wrap.h points STBDS_OUT_OF_MEMORY at it wherever the stb_ds implementation is compiled. It logs "out of memory (N bytes) in dynamic array" via dvxLog and raises SIGSEGV, so under platformInstallCrashHandler a failed growth takes the normal crash-recovery path: the shell terminates the app that was current (its task, or its callback running on the shell task), and a failure with no app current is logged and the shell returns to its main loop. With no handler installed (the loader, the tools, the BASIC compiler) the process terminates. It never returns. The stb_ds patch that adds the hook is recorded in thirdparty/stb_ds.PATCHES.md.
     
     .h2 VGA Splash Screen
     
    diff --git a/src/libs/kpunch/libdvx/arch.dhs b/src/libs/kpunch/libdvx/arch.dhs
    index 62f4baa1..1a2d27b4 100644
    --- a/src/libs/kpunch/libdvx/arch.dhs
    +++ b/src/libs/kpunch/libdvx/arch.dhs
    @@ -401,7 +401,7 @@ Pre-built modal dialog boxes block the caller and run their own event loop via d
       dvxMessageBox         Generic message box with button/icon flags (MB_OK, MB_YESNO, MB_ICONERROR, etc.)
       dvxErrorBox           Shortcut for MB_OK + MB_ICONERROR
       dvxInfoBox            Shortcut for MB_OK + MB_ICONINFO
    -  dvxFileDialog         File open/save dialog with directory navigation, a drive dropdown and a filter dropdown
    +  dvxFileDialog         File open/save dialog with directory navigation, a drive dropdown, and a filter dropdown
       dvxInputBox           Single-line text input with OK/Cancel
       dvxIntInputBox        Integer input with spinner, clamp range, and step
       dvxChoiceDialog       List selection with OK/Cancel
    @@ -448,6 +448,7 @@ Readers start from the end: seek to EOF - sizeof(footer), verify the magic (DVX_
       DVX_RES_ICON      1       Image data (BMP or PNG, typically 16x16 or 32x32 icons)
       DVX_RES_TEXT      2       Null-terminated string (localisable UI text, author, copyright)
       DVX_RES_BINARY    3       Arbitrary data (app-specific)
    +  DVX_RES_FONT      4       A .DFN font the program carries (the shell adds it for the program when it loads it)
     .endtable
     
     .h2 Runtime API
    @@ -616,7 +617,7 @@ DVX uses a cooperative polling model. The main loop (dvxRun / dvxUpdate) runs th
     .item Poll mouse -- platformMousePoll() returns position and button bitmask. Compare with previous frame for press/release edge detection.
     .item Poll keyboard -- platformKeyboardRead() returns ASCII + scancode. Non-blocking; returns false if buffer is empty. Shift/Ctrl/Alt+F-keys arrive as the plain F-key plus modifier flags. If an input entropy hook is installed (dvxSetInputEntropyHook), input events are timestamped and fed to it here.
     .item Dispatch to focused window -- the event loop fires window callbacks (onKey, onMouse, etc.) on the focused window. If the window has a widget tree, the widget system's installed handlers dispatch to individual widgets.
    -.item Tooltips, widget polling (WCLASS_NEEDS_POLL), cursor blink and widget timers.
    +.item Tooltips, widget polling (WCLASS_NEEDS_POLL), cursor blink, and widget timers.
     .item Deferred paint flush -- every window whose paintNeeded was raised by wgtInvalidate / wgtInvalidatePaint during the frame is painted once (the highest requested level wins). Windows queued for destruction are skipped.
     .item Deferred destroy flush -- windows destroyed from inside callbacks are torn down once the dispatch depth is back to zero.
     .item Compositor pass -- merge dirty rects, composite, flush to LFB.
    @@ -649,7 +650,7 @@ DVX uses a cooperative polling model. The main loop (dvxRun / dvxUpdate) runs th
       Universal callbacks (onClick, onChange, etc.)
     .endcode
     
    -Mouse events use two hit tests. widgetHitTest finds the widget whose class handles the press; it stops at a WCLASS_NO_HIT_RECURSE container (Splitter, ScrollPane), so the container's ON_MOUSE handler still gets the press (divider drag, scrollbar clicks, forwarding into its children). widgetCallbackTarget continues through non-focusable NO_HIT_RECURSE containers into the child whose paintClip (the visible part when it was last painted) contains the point. The universal callbacks (onClick, onDblClick, onMouseDown/Up/Move), context menus, tooltips and the cursor shape use that target, so a control inside a Splitter pane or ScrollPane viewport gets its own events only where it is visible; on the container's own chrome the container is the target.
    +Mouse events use two hit tests. widgetHitTest finds the widget whose class handles the press; it stops at a WCLASS_NO_HIT_RECURSE container (Splitter, ScrollPane), so the container's ON_MOUSE handler still gets the press (divider drag, scrollbar clicks, forwarding into its children). widgetCallbackTarget continues through non-focusable NO_HIT_RECURSE containers into the child whose paintClip (the visible part when it was last painted) contains the point. The universal callbacks (onClick, onDblClick, onMouseDown/Up/Move), context menus, tooltips, and the cursor shape use that target, so a control inside a Splitter pane or ScrollPane viewport gets its own events only where it is visible; on the container's own chrome the container is the target.
     
     .h2 Accelerator Tables
     
    @@ -683,42 +684,117 @@ Timestamp-based: two clicks on the same target (title bar, minimized icon, close
     .index BitmapFontT
     .index Text Rendering
     .index CP437
    +.index DFN
    +.index Font files
    +.index mkfont
    +.index Loaded fonts
    +.index Font smoothing
    +.index Anti-aliasing
     
     .h1 Font System
     
    -DVX uses fixed-width 8-pixel-wide bitmap fonts only. One size is provided: 8x16, matching the standard VGA ROM font and CP437 encoding (256 glyphs).
    +Every text call takes a BitmapFontT, and there are two kinds of them. The system font is the 8x16 VGA ROM font (code page 437, 256 glyphs) built into libdvx: 8-pixel cells, one byte per glyph row, so character positions are a multiplication, glyph lookup an index, and every scanline a single byte -- the hot paths of drawChar, drawTextN, and drawTermRow depend on it. A loaded font (dvxText.h) is one size of a font family from the FONTS folder (Setup installs none, the base set of DejaVu Sans, Sans Mono, and Serif, or all of them; a family that is not there opens as the system font): per-glyph advances and bitmap boxes, any width, proportional or fixed-pitch, and in up to four styles. The same calls draw and measure both; a loaded font's glyphs field is what sends them to the general paths.
     
     .h2 BitmapFontT
     
     .code
       typedef struct {
    -      int32_t        charWidth;    // fixed width per glyph (always 8)
    -      int32_t        charHeight;   // 16
    -      int32_t        firstChar;    // typically 0
    -      int32_t        numChars;     // typically 256
    -      const uint8_t *glyphData;    // packed 1bpp, charHeight bytes per glyph
    +      int32_t           charWidth;    // cell width (fixed pitch) or the digit 0's advance
    +      int32_t           charHeight;   // line height
    +      int32_t           firstChar;    // 0
    +      int32_t           numChars;     // 256
    +      const uint8_t    *glyphData;    // system font: charHeight bytes per glyph; loaded: bitmaps
    +      const FontGlyphT *glyphs;       // loaded font: per-glyph metrics; NULL: the system font
    +      const uint32_t   *glyphAt;      // loaded font: each glyph's bitmap offset
    +      int32_t           ascent;       // top of a line to the baseline
    +      int32_t           pixelSize;
    +      uint32_t          style;        // FONT_STYLE_BOLD / ITALIC
    +      uint32_t          flags;        // FONT_FLAG_PROPORTIONAL, _SYNTH_BOLD, _SYNTH_ITALIC, _GRAY
    +      int32_t           underlineY;   // underline and accelerator row
    +      int32_t           strikeY;      // strike-through row
    +      int32_t           lineThick;
    +      void             *priv;         // the font cache's entry
    +      uint32_t          serial;       // new whenever the glyphs are built (again)
       } BitmapFontT;
     .endcode
     
    -Design rationale:
    -
    -.list
    -.item Character positions are pure multiplication (x = col * 8).
    -.item Glyph lookup is a single array index.
    -.item Each scanline of a glyph is exactly one byte (1bpp at 8 pixels wide).
    -.item No glyph-width tables, kerning, or per-character positioning needed.
    -.item 8-pixel width aligns with byte boundaries -- no bit shifting in per-scanline rendering.
    -.endlist
    +A FontGlyphT is six bytes: advance, bitmap width and height, the bitmap's offset from the pen (left, and top up from the baseline), and flags. A glyph's bitmap is height rows of (width + 7) / 8 bytes, the leftmost pixel in the top bit.
     
     .h2 Text Rendering Functions
     
     drawChar() -- Renders a single character. Supports opaque (background fill) and transparent modes.
     
    -drawTextN() -- Optimized batch rendering for a known character count. Clips once for the entire run, fills background in a single rectFill, then overlays glyph foreground pixels. Significantly faster than per-character rendering for long runs.
    +drawTextN() -- Optimized batch rendering for a known character count. Clips once for the entire run, fills background in a single rectFill, then overlays glyph foreground pixels. Significantly faster than per-character rendering for long runs. In a loaded font each glyph is drawn at the pen; every bitmap byte whose 8 pixels lie inside the clip rectangle goes through the same unrolled row writers as the system font, so only the bytes on a clip edge are drawn pixel by pixel.
     
     drawTermRow() -- Renders an 80-column terminal row in a single pass, with per-cell foreground/background from a 16-color palette, blink attribute support, and cursor rendering. Exists because per-character terminal rendering is unacceptably slow on target hardware.
     
    -drawTextAccel() -- Renders text with & accelerator markers. The character after & is underlined to indicate the keyboard shortcut.
    +drawTextAccel() -- Renders text with & accelerator markers. The character after & is marked with the font's underline across its advance to indicate the keyboard shortcut.
    +
    +drawTextStyled() -- Text with an underline and / or strike-through as wide as it, at the font's rows.
    +
    +textWidth(), textWidthN(), textWidthAccel() -- Widths: for the system font a multiplication, for a loaded font the sum of the advances.
    +
    +.h2 Loading Fonts
    +
    +dvxFontOpen(family, size, style) reads the family list from FONTS (once, at the first call: each .DFN file's header and size table), picks the face for the style -- the exact one, else one with fewer style bits whose missing bits are made -- and that face's size nearest the one asked for. It never scales. The font is built once into a glyph table and bitmaps of its own, so drawing never decides anything per glyph:
    +
    +.list
    +.item A character the face has is copied; when a style is made, bold thickens each stroke by a column (two from 24 pixels) and italic moves each row one column right per four rows above the baseline.
    +.item Box drawing, block, and shade characters (CP437 B0-DF) the face lacks -- or all of them, when its file asks, or when a made bold widens a fixed pitch -- are drawn to fill the character cell from a table of each character's arms (none, single, or double; up, down, left, and right), so lines join across characters. They are never thickened or slanted.
    +.item Any other character the face lacks comes from DejaVu Sans (DejaVu Sans Mono for a fixed-pitch face, centered in its pitch) at the nearest size in the same style, or from the system font when those are not installed.
    +.item What nothing has is a blank as wide as a space.
    +.endlist
    +
    +Fonts are cached and counted: the same family, size, and style opened again is the same font. Up to eight fonts no one holds stay loaded, the least recently used unloaded first. Everything the font system allocates is charged to the system, not to the program that happened to open a font.
    +
    +.h2 Fonts a Program Carries
    +
    +A family is installed (the FONTS folder, read by its full path so a program that changes the current folder still finds them) or added for an owner: a program's app id, or 0 for every program. The shell calls dvxFontAddResources for each program it loads (its DVX_RES_FONT resources, read in place: a font source is a file and the font's offset and length in it) and dvxFontRemoveOwner when its resources are released; a program can add .DFN files or fonts in memory itself. Every lookup is for the running program (*dvxMemAppIdPtr, the shell's currentAppId; window callbacks run as the window's program).
    +
    +Opening a font picks its face per style from the program's own family and the installed one together: the style's faces in sFaceOrder's order, the first style either has; then the nearest size of either face's sizes; then the program's face when it has that size, else the installed one. So a program's faces and sizes win, and the installed family fills in the rest. A program with no families of its own shares the cache with every other; one with its own gets fonts built for it alone (their fallback glyphs may come from its families too). A program's fonts that change or go are detached from the cache: the ones no one holds unloaded, held ones kept as they are.
    +
    +Each face holds its source counted, and each font built from it holds it too, so a font's licence (dvxFontLicence) is always the one of the face it was built from, its program ended or not.
    +
    +The building works on one level per pixel (0 to FONT_GRAY_FULL) whatever the source, and packs the finished glyph at the font's own depth, so made styles, box characters, and fallback glyphs are the same code for both kinds of font.
    +
    +.h2 Smoothed Text
    +
    +A .DFN size can come twice: 1-bit, and smoothed -- FONT_GRAY_BPP (2) bits per pixel, four levels from none to full, rendered by FreeType with light hinting. mkfont smooths the sizes fonts.ini lists under smooth. A 1-bit size is FreeType's 1-bit hinted rendering, except at the sizes listed under graycut, where it is the light-hinted rendering with every pixel at least half covered set: some faces' 1-bit hinting draws them too light (DejaVu Serif Bold at its smoothed sizes, which comes out near regular weight, and DejaVu Sans Bold at 8 pixels). When smoothing is on (dvxFontSetSmooth), such a size opens from its smoothed strike and the font has FONT_FLAG_GRAY; every other size, and every face without one, stays 1-bit.
    +
    +Drawing a full-level pixel is a store of the text color, as for a 1-bit font. A partial one is the text color blended into the pixel under it, channel by channel through the display's PixelFormatT masks and shifts; text mostly stands on one color, so each glyph keeps its last blend per level and reuses it while the color under it stays the same. Only partial pixels cost more than 1-bit text. At 8 bits per pixel there is nothing to blend with: the pixels of FONT_GRAY_HALF and more are drawn and the rest dropped. The scaled text path (drawTextScaled) does the same.
    +
    +The application decides (dvxSetFontSmoothing): smoothing is on when the user wants it (DVX.INI [fonts] smoothing, on by default; Control Panel's Video page) and the screen has more than 8 bits per pixel, and a video mode change applies it again. Turning it on or off builds every open font again in place: widgets keep their font pointers, the glyphs change under them, and the font's serial changes so caches of measurements (list column widths and the like) know to measure again. Every window is then laid out and painted again.
    +
    +.h2 Widget Fonts
    +
    +Any widget can have a font of its own (wgtSetFont; BASIC's FontName, FontSize, FontBold ... properties), and the widgets inside a container draw with the container's font unless they have their own, so a form's font is the default for everything on it. wgtFontFor(w, inherited) walks up the parent chain to the nearest font set, else takes the one passed down (the application's). The class dispatch helpers hand every paint, measure, and layout method that font, so widget code just uses its font parameter -- the same code serves the system font, a proportional font, and a fixed-pitch one of any size. Event handlers, which are given no font, call wgtFont(w). Widgets measure text with textWidth / textWidthN, never with character counts. A class that edits text by columns (TextBox, Terminal, ComboBox, Spinner: WCLASS_FIXED_FONT) given a proportional font draws with the system font instead; a fixed-pitch font of any size works there, its column math using that font's charWidth and charHeight.
    +
    +Underline and strike-through are part of a font, as in Visual Basic: a font opened with them shares the glyphs of the same font without them, and every text call draws its lines. A widget holds its font open while it is set and closes it when destroyed (widgetFree, the one place a widget is freed).
    +
    +.h2 Rich Text
    +
    +Text in several fonts in one control is built on the text flow (dvxFlow.h): runs of text, each in a font and a color, laid out into wrapped, aligned lines as tall as the tallest font on them, with offsets for carets, hit tests, and selections. The RichLabel widget is one flow built by calls. The RichEdit widget keeps its document in the richtext library (styled text: runs of interned styles over code page 437 text, an alignment per paragraph, copy / replace / restyle, and RTF in and out) and lays each paragraph out as a flow of its own, so an edit lays out only the paragraphs it touched and repaints only the rows they hold (its quick repaint); a change of height repaints the widget. Its undo steps are pieces of styled text swapped back in, and Copy keeps a styled copy for a paste of the same text.
    +
    +.h2 Font Files (.DFN)
    +
    +One file holds one family in one style at every size it comes in, made on the build host by mkfont (src/tools/mkfont.c) with FreeType from TrueType, OpenType, Type 1, BDF, and PCF fonts, as listed in src/fonts/fonts.ini. A .DFN is a pure format conversion: the converter never adds or changes a glyph, it only marks the characters a face lacks. Little-endian, every record read whole:
    +
    +.table
    +  Part                      Size            Holds
    +  ----                      ----            -----
    +  DfnHeaderT                60 bytes        "DVXF", format version (2), style, face flags (fixed pitch; box characters drawn by DVX), strike count (1 to 32), family name (31 characters), where the notice and licence texts are and their lengths
    +  DfnStrikeT x count        24 bytes each   per size, smallest first: where its glyph table and bitmaps are, the bitmaps' size, the pixel size, ascent, descent, line height, widest advance
    +  FontGlyphT x 256          6 bytes each    per size, at its glyph table offset
    +  glyph bitmaps             as listed       per size, back to back in character order
    +  notice text               as listed       the family and style, the source fonts it was converted from, and their copyright notices
    +  licence text              as listed       the licence of the font it came from
    +.endtable
    +
    +Each glyph's bitmap starts where the one before ends, so the reader (dvxFontFmt.c) works out the offsets and checks that they fill the strike's bitmaps exactly. Every count, offset, and size is checked against the font's length before it is used: a damaged file is refused, never read past.
    +
    +Each file carries what its licence asks to go with every copy of the font: the notice and licence texts, transliterated to ASCII with CR LF line ends. They are read only when asked for (dvxFontLicence, the Font dialog's Licence button), never when text is drawn, so they cost nothing at run time. A font goes nowhere without its licence: copied, installed, or carried inside a program as a resource.
    +
    +The reader takes a DfnSourceT: a file of its own, a font inside another file at an offset (a program's font resource), or a font in memory. dfnOpen reads the header and strike table, dfnLoadStrike one size, and dfnLoadText a text.
     
     .h2 Performance Optimization
     
    @@ -824,7 +900,7 @@ Six functions: platformSpanFill8/16/32() and platformSpanCopy8/16/32(). Called o
     
     .h3 Mouse Input
     
    -Polling model. platformMousePoll() returns position and button bitmask. Wheel support via CuteMouse API. platformMouseInit() can be called again after a video-mode change; it re-applies the speed, acceleration and wheel settings after the driver reset.
    +Polling model. platformMousePoll() returns position and button bitmask. Wheel support via CuteMouse API. platformMouseInit() can be called again after a video-mode change; it re-applies the speed, acceleration, and wheel settings after the driver reset.
     
     .h3 Keyboard Input
     
    diff --git a/src/libs/kpunch/libdvx/dvxApp.c b/src/libs/kpunch/libdvx/dvxApp.c
    index 03785799..666a0db8 100644
    --- a/src/libs/kpunch/libdvx/dvxApp.c
    +++ b/src/libs/kpunch/libdvx/dvxApp.c
    @@ -57,6 +57,7 @@
     #include "../../../widgets/kpunch/timer/timer.h"
     #include "dvxFont.h"
     #include "dvxRes.h"
    +#include "dvxText.h"
     #include "dvxPrefs.h"
     #include "thirdparty/stb_ds_wrap.h"
     
    @@ -156,8 +157,8 @@ DVX_ABI_STAMP(libdvx);
     #define THEME_SECTION          "colors"
     #define THEME_VALUE_MAX        16    // "255,255,255" plus terminator
     
    -// DVX.INI [mouse] and [desktop] keys (dvxMouseFromPrefs / ToPrefs,
    -// dvxWallpaperFromPrefs / ToPrefs)
    +// DVX.INI [mouse], [desktop] and [fonts] keys (dvxMouseFromPrefs / ToPrefs,
    +// dvxWallpaperFromPrefs / ToPrefs, dvxFontsFromPrefs / ToPrefs)
     #define MOUSE_SECTION          "mouse"
     #define MOUSE_KEY_WHEEL        "wheel"
     #define MOUSE_KEY_DBLCLICK     "doubleclick"
    @@ -167,6 +168,12 @@ DVX_ABI_STAMP(libdvx);
     #define DESKTOP_SECTION        "desktop"
     #define DESKTOP_KEY_WALLPAPER  "wallpaper"
     #define DESKTOP_KEY_MODE       "mode"
    +#define FONTS_SECTION          "fonts"
    +#define FONTS_KEY_SMOOTHING    "smoothing"
    +
    +// Smoothed text is drawn in blends of the text and background colors:
    +// direct color only (more than 8 bits per pixel)
    +#define FONT_SMOOTH_MIN_BPP    15
     
     // djb2 text hash parameters
     #define DJB2_SEED              5381
    @@ -303,6 +310,7 @@ static void flushWindowPaint(AppContextT *ctx, WindowT *win);
     static WindowT *focusedWindow(const AppContextT *ctx);
     static void focusTopmostAlive(AppContextT *ctx);
     static void focusWindowAt(AppContextT *ctx, int32_t idx);
    +static bool fontSmoothSet(const AppContextT *ctx);
     static void handleKbMoveResizeKey(AppContextT *ctx, int32_t scancode, int32_t ascii);
     static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons);
     static void handlePopupKey(AppContextT *ctx, int32_t scancode, int32_t ascii);
    @@ -2841,6 +2849,10 @@ int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t request
             ctx->mouseY = ctx->display.height - 1;
         }
     
    +    // Smoothed text or not at the new depth (the loop below lays out and
    +    // paints every window)
    +    fontSmoothSet(ctx);
    +
         // Clamp and reallocate all window content buffers
         for (int32_t i = 0; i < ctx->stack.count; i++) {
             WindowT *win = ctx->stack.windows[i];
    @@ -3392,6 +3404,24 @@ void dvxFitWindowW(AppContextT *ctx, WindowT *win) {
     }
     
     
    +// ============================================================
    +// dvxFontsFromPrefs
    +// ============================================================
    +
    +void dvxFontsFromPrefs(AppContextT *ctx, PrefsHandleT *h) {
    +    dvxSetFontSmoothing(ctx, prefsGetBool(h, FONTS_SECTION, FONTS_KEY_SMOOTHING, true));
    +}
    +
    +
    +// ============================================================
    +// dvxFontsToPrefs
    +// ============================================================
    +
    +void dvxFontsToPrefs(const AppContextT *ctx, PrefsHandleT *h) {
    +    prefsSetBool(h, FONTS_SECTION, FONTS_KEY_SMOOTHING, ctx->fontSmoothing);
    +}
    +
    +
     // ============================================================
     // dvxFreeAccelTable
     // ============================================================
    @@ -3632,8 +3662,11 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_
         ctx->dblClickTicks      = PLATFORM_MS_TO_TICKS(MOUSE_DBLCLICK_DEFAULT_MS);
         ctx->mouseAccel         = prefsAccelChoices[prefsChoiceIndex(prefsAccelChoices, PREFS_ACCEL_CHOICE_COUNT, MOUSE_ACCEL_DEFAULT)].value;
         ctx->mouseMickeys       = MOUSE_SPEED_DEFAULT;
    +    ctx->fontSmoothing      = true;
         sDblClickTicks          = ctx->dblClickTicks;
     
    +    fontSmoothSet(ctx);
    +
         // Pre-compute fixed-point 16.16 reciprocal of character height so
         // popup menu item index calculation can use multiply+shift instead
         // of division. On a 486, integer divide is 40+ cycles; this
    @@ -4324,6 +4357,22 @@ void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b)
     }
     
     
    +// ============================================================
    +// dvxSetFontSmoothing
    +// ============================================================
    +//
    +// The open fonts are built again in place, so text measured with them
    +// is laid out again: every window is.
    +
    +void dvxSetFontSmoothing(AppContextT *ctx, bool on) {
    +    ctx->fontSmoothing = on;
    +
    +    if (fontSmoothSet(ctx)) {
    +        invalidateAllWindows(ctx);
    +    }
    +}
    +
    +
     // ============================================================
     // dvxSetInputEntropyHook
     // ============================================================
    @@ -5390,6 +5439,20 @@ static void focusWindowAt(AppContextT *ctx, int32_t idx) {
     }
     
     
    +// Smooth the fonts (dvxFontSetSmooth) when the user wants it and the
    +// screen has direct color; true when that changed what text looks like
    +static bool fontSmoothSet(const AppContextT *ctx) {
    +    bool smooth = ctx->fontSmoothing && ctx->display.format.bitsPerPixel >= FONT_SMOOTH_MIN_BPP;
    +
    +    if (smooth == dvxFontSmooth()) {
    +        return false;
    +    }
    +
    +    dvxFontSetSmooth(smooth);
    +    return true;
    +}
    +
    +
     // Keyboard move/size mode (entered from the system menu): arrows move or
     // size the window by KB_MOVE_STEP, Enter confirms, Esc restores the
     // original geometry.  Every key is consumed while the mode is active.
    diff --git a/src/libs/kpunch/libdvx/dvxApp.h b/src/libs/kpunch/libdvx/dvxApp.h
    index 47bf122f..bc793c80 100644
    --- a/src/libs/kpunch/libdvx/dvxApp.h
    +++ b/src/libs/kpunch/libdvx/dvxApp.h
    @@ -158,6 +158,7 @@ typedef struct AppContextT {
         void         *titleChangeCtx;
         int32_t      currentAppId;   // set by shell before calling app code (0 = shell)
         bool         busy;           // when true, show hourglass and block input
    +    bool         fontSmoothing;  // smooth text when the screen allows (dvxSetFontSmoothing)
         // Tooltip state  -- tooltip appears after the mouse hovers over a widget
         // with a tooltip string for a brief delay. Pre-computing W/H avoids
         // re-measuring on every paint frame.
    @@ -221,6 +222,18 @@ void dvxMouseFromPrefs(AppContextT *ctx, PrefsHandleT *h);
     // keys dvxMouseFromPrefs reads).
     void dvxMouseToPrefs(const AppContextT *ctx, PrefsHandleT *h);
     
    +// Smooth (anti-alias) text, or stop: on (the default) it is smoothed at
    +// more than 8 bits per pixel, in the faces and sizes the FONTS folder
    +// has smoothed.  Every window is laid out and painted again when what is
    +// drawn changes; a video mode change applies it to the new depth.
    +void dvxSetFontSmoothing(AppContextT *ctx, bool on);
    +
    +// Apply the DVX.INI [fonts] settings in h: smoothing (on when missing).
    +void dvxFontsFromPrefs(AppContextT *ctx, PrefsHandleT *h);
    +
    +// Store the font settings as [fonts] keys of h.
    +void dvxFontsToPrefs(const AppContextT *ctx, PrefsHandleT *h);
    +
     // Input-timing entropy sink.  When set, every keyboard event and every
     // mouse change (move, button, wheel) is timestamped with a fine-grained
     // platform counter and passed, in batches of a few events, to fn (e.g. a
    diff --git a/src/libs/kpunch/libdvx/dvxDialog.c b/src/libs/kpunch/libdvx/dvxDialog.c
    index 9ba9b3db..4b7d9bc6 100644
    --- a/src/libs/kpunch/libdvx/dvxDialog.c
    +++ b/src/libs/kpunch/libdvx/dvxDialog.c
    @@ -44,12 +44,16 @@
     // state.
     
     #include "dvxDlg.h"
    +#include "dvxFontFmt.h"
    +#include "dvxText.h"
     #include "dvxPlat.h"
     #include "dvxWgt.h"
     #include "dvxWgtP.h"
    +#include "stb_ds_wrap.h"
     #include "../../../widgets/kpunch/box/box.h"
     #include "../../../widgets/kpunch/button/button.h"
     #include "../../../widgets/kpunch/canvas/canvas.h"
    +#include "../../../widgets/kpunch/checkbox/checkbox.h"
     #include "../../../widgets/kpunch/dropdown/dropdown.h"
     #include "../../../widgets/kpunch/label/label.h"
     #include "../../../widgets/kpunch/listBox/listBox.h"
    @@ -59,6 +63,7 @@
     #include "../../../widgets/kpunch/textInput/textInpt.h"
     
     #include 
    +#include 
     #include 
     #include 
     #include 
    @@ -133,6 +138,20 @@
     #define IB_MIN_BUF_SIZE    2     // one character plus the terminator
     
     // Choice dialog layout constants
    +// Font dialog layout (its style list is indexed by the style bits)
    +#define FN_STYLE_COUNT   (FONT_STYLE_FACE + 1)
    +#define FN_FAMILY_W      150
    +#define FN_STYLE_W       90
    +#define FN_SIZE_W        50
    +#define FN_LIST_H        120
    +#define FN_SAMPLE_H      56
    +#define FN_GAP           6
    +#define FN_SIZE_TEXT     8                     // a size's decimal text with its NUL
    +#define FN_SAMPLE_TEXT   "AaBbYyZz 123"
    +#define FN_LICENCE_W     520                   // the licence viewer's text box
    +#define FN_LICENCE_H     260
    +#define FN_TITLE_MAX     (DFN_FAMILY_MAX + 16) // " Licence" with its NUL
    +
     #define CB_DIALOG_WIDTH  200
     #define CB_LIST_HEIGHT   120
     #define CB_PADDING       8
    @@ -251,6 +270,25 @@ typedef struct {
         WidgetT    *listBox;
     } ChoiceBoxStateT;
     
    +typedef struct {
    +    DialogBaseT     base;                                   // must be first (window userData)
    +    AppContextT    *ctx;
    +    DvxFontChoiceT *out;
    +    DvxFontChoiceT  cur;
    +    const char    **families;                               // stb_ds; names owned by the font system
    +    const char     *sizeItems[DFN_STRIKE_MAX];
    +    char            sizeText[DFN_STRIKE_MAX][FN_SIZE_TEXT];
    +    int32_t         sizes[DFN_STRIKE_MAX];
    +    int32_t         sizeCount;
    +    WidgetT        *familyList;
    +    WidgetT        *styleList;
    +    WidgetT        *sizeList;
    +    WidgetT        *underline;
    +    WidgetT        *strike;
    +    WidgetT        *licence;
    +    WidgetT        *sample;
    +} FontBoxStateT;
    +
     typedef struct {
         DialogBaseT  base;                          // must be first (window userData)
         AppContextT *ctx;
    @@ -315,6 +353,9 @@ static const uint32_t sBasicColors[CD_PALETTE_COUNT] = {
     // The color dialog's channel labels, in cdPacked order.
     static const char *const sChannelLabels[CD_CHANNEL_COUNT] = { "&Red:", "&Green:", "&Blue:" };
     
    +// The font dialog's styles, by style bits
    +static const char *sFontStyles[FN_STYLE_COUNT] = { "Regular", "Bold", "Italic", "Bold Italic" };
    +
     
     // ============================================================
     // Prototypes
    @@ -360,6 +401,13 @@ static void        fdOnListDblClick(WidgetT *w);
     static void        fdOnOk(WidgetT *w);
     static void        fdOnPathKey(WidgetT *w, int32_t keyCode, int32_t shift);
     static bool        fdValidateFilename(FileDialogStateT *fd, const char *name);
    +static WidgetT    *fnAddList(WidgetT *row, const char *label, int32_t width);
    +static void        fnOnChange(WidgetT *w);
    +static void        fnOnLicence(WidgetT *w);
    +static void        fnOnOk(WidgetT *w);
    +static void        fnRefreshSample(FontBoxStateT *fn);
    +static void        fnRefreshSizes(FontBoxStateT *fn);
    +static void        fnShowLicence(AppContextT *ctx, const char *family, char *text);
     static void        ibOnOk(WidgetT *w);
     static void        iibOnOk(WidgetT *w);
     static int32_t     msgBoxRun(AppContextT *ctx, const char *title, const char *message, int32_t iconFlags, const char * const *btnLabels, const int32_t *btnIds, int32_t btnCount, int32_t closeResult);
    @@ -1136,6 +1184,100 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
     }
     
     
    +bool dvxFontDialog(AppContextT *ctx, const char *title, uint32_t flags, DvxFontChoiceT *choiceInOut) {
    +    if (!ctx || !choiceInOut) {
    +        return false;
    +    }
    +
    +    FontBoxStateT fn;
    +    int32_t       current = 0;
    +
    +    memset(&fn, 0, sizeof(fn));
    +    fn.ctx = ctx;
    +    fn.out = choiceInOut;
    +    fn.cur = *choiceInOut;
    +
    +    // The families offered, and the starting one among them
    +    for (int32_t i = 0; i < dvxFontFamilyCount(); i++) {
    +        const char *name = dvxFontFamilyName(i);
    +
    +        if ((flags & DVX_FONT_DLG_FIXED) && !dvxFontFamilyFixed(name)) {
    +            continue;
    +        }
    +
    +        if (strcasecmp(name, choiceInOut->family) == 0) {
    +            current = (int32_t)arrlen(fn.families);
    +        }
    +
    +        arrput(fn.families, name);
    +    }
    +
    +    WindowT *win = dialogCreate(ctx, title ? title : "Font", FN_FAMILY_W + FN_STYLE_W + FN_SIZE_W + FN_GAP * 2, DLG_NOMINAL_H, &fn.base);
    +
    +    if (!win) {
    +        arrfree(fn.families);
    +        return false;
    +    }
    +
    +    WidgetT *root    = win->widgetRoot;
    +    WidgetT *lists   = wgtHBox(root);
    +    WidgetT *effects = lists ? wgtHBox(root) : NULL;
    +    WidgetT *frame   = effects ? wgtFrame(root, "Sample") : NULL;
    +
    +    if (lists) {
    +        lists->spacing = wgtPixels(FN_GAP);
    +        fn.familyList  = fnAddList(lists, "&Font:", FN_FAMILY_W);
    +        fn.styleList   = fnAddList(lists, "Font st&yle:", FN_STYLE_W);
    +        fn.sizeList    = fnAddList(lists, "&Size:", FN_SIZE_W);
    +    }
    +
    +    if (effects) {
    +        effects->spacing = wgtPixels(FN_GAP);
    +        fn.underline     = wgtCheckbox(effects, "&Underline");
    +        fn.strike        = wgtCheckbox(effects, "Stri&keout");
    +        wgtSpacer(effects);
    +        fn.licence       = wgtButton(effects, "&Licence...");
    +    }
    +
    +    if (frame) {
    +        frame->minH = wgtPixels(FN_SAMPLE_H);
    +        frame->maxH = wgtPixels(FN_SAMPLE_H);
    +        fn.sample   = wgtLabel(frame, FN_SAMPLE_TEXT);
    +    }
    +
    +    // Every control must exist: the callbacks use them all
    +    if (!fn.familyList || !fn.styleList || !fn.sizeList || !fn.underline || !fn.strike || !fn.licence || !fn.sample) {
    +        dialogDestroy(ctx, win);
    +        arrfree(fn.families);
    +        return false;
    +    }
    +
    +    snprintf(fn.cur.family, sizeof(fn.cur.family), "%s", fn.families[current]);
    +    wgtListBoxSetItems(fn.familyList, fn.families, (int32_t)arrlen(fn.families));
    +    wgtListBoxSetSelected(fn.familyList, current);
    +    wgtListBoxSetItems(fn.styleList, sFontStyles, FN_STYLE_COUNT);
    +    wgtListBoxSetSelected(fn.styleList, (int32_t)(fn.cur.style & FONT_STYLE_FACE));
    +    wgtCheckboxSetChecked(fn.underline, (fn.cur.style & FONT_STYLE_UNDERLINE) != 0);
    +    wgtCheckboxSetChecked(fn.strike, (fn.cur.style & FONT_STYLE_STRIKE) != 0);
    +    fnRefreshSizes(&fn);
    +
    +    fn.familyList->onChange = fnOnChange;
    +    fn.styleList->onChange  = fnOnChange;
    +    fn.sizeList->onChange   = fnOnChange;
    +    fn.underline->onChange  = fnOnChange;
    +    fn.strike->onChange     = fnOnChange;
    +    fn.licence->onClick     = fnOnLicence;
    +
    +    wgtSetFocused(fn.familyList);
    +    dialogAddOkCancel(root, &fn.base, "&OK", fnOnOk, AlignCenterE);
    +    dvxFitWindow(ctx, win);
    +    dialogCenter(ctx, win);
    +    dialogRunModal(ctx, win, &fn.base);
    +    arrfree(fn.families);
    +    return fn.base.accepted;
    +}
    +
    +
     // Convenience wrapper: MB_OK | MB_ICONINFO, title "Information" when NULL.
     
     int32_t dvxInfoBox(AppContextT *ctx, const char *title, const char *message) {
    @@ -1870,6 +2012,167 @@ static bool fdValidateFilename(FileDialogStateT *fd, const char *name) {
     }
     
     
    +// A column of the font dialog: its label over a list width pixels wide.
    +// Returns the list (NULL on failure).
    +static WidgetT *fnAddList(WidgetT *row, const char *label, int32_t width) {
    +    WidgetT *col  = wgtVBox(row);
    +    WidgetT *list = (col && wgtLabel(col, label)) ? wgtListBox(col) : NULL;
    +
    +    if (list) {
    +        list->minW = wgtPixels(width);
    +        list->minH = wgtPixels(FN_LIST_H);
    +    }
    +
    +    return list;
    +}
    +
    +
    +// A list or box changed: the choice follows it; a new family or style
    +// has its own sizes
    +static void fnOnChange(WidgetT *w) {
    +    FontBoxStateT *fn     = (FontBoxStateT *)w->window->userData;
    +    int32_t        family = wgtListBoxGetSelected(fn->familyList);
    +    int32_t        style  = wgtListBoxGetSelected(fn->styleList);
    +    int32_t        size   = wgtListBoxGetSelected(fn->sizeList);
    +
    +    if (family >= 0) {
    +        snprintf(fn->cur.family, sizeof(fn->cur.family), "%s", fn->families[family]);
    +    }
    +
    +    fn->cur.style = (uint32_t)(style > 0 ? style : 0);
    +
    +    if (wgtCheckboxIsChecked(fn->underline)) {
    +        fn->cur.style |= FONT_STYLE_UNDERLINE;
    +    }
    +
    +    if (wgtCheckboxIsChecked(fn->strike)) {
    +        fn->cur.style |= FONT_STYLE_STRIKE;
    +    }
    +
    +    if (w == fn->familyList || w == fn->styleList) {
    +        fnRefreshSizes(fn);
    +        return;
    +    }
    +
    +    if (w == fn->sizeList && size >= 0) {
    +        fn->cur.size = fn->sizes[size];
    +    }
    +
    +    fnRefreshSample(fn);
    +}
    +
    +
    +// Licence...: the chosen font's notice and licence (the system font's is
    +// DVX's own)
    +static void fnOnLicence(WidgetT *w) {
    +    FontBoxStateT *fn   = (FontBoxStateT *)w->window->userData;
    +    BitmapFontT   *font = dvxFontOpen(fn->cur.family, fn->cur.size, fn->cur.style & FONT_STYLE_FACE);
    +    char          *text = dvxFontLicence(font);
    +
    +    dvxFontClose(font);
    +
    +    if (!text) {
    +        dvxInfoBox(fn->ctx, "Font Licence", "The System font is DVX's own: it comes with DVX, under DVX's licence.");
    +        return;
    +    }
    +
    +    fnShowLicence(fn->ctx, fn->cur.family, text);
    +    free(text);
    +}
    +
    +
    +static void fnOnOk(WidgetT *w) {
    +    FontBoxStateT *fn = (FontBoxStateT *)w->window->userData;
    +
    +    *fn->out          = fn->cur;
    +    fn->base.accepted = true;
    +    fn->base.done     = true;
    +}
    +
    +
    +// The sample in the font chosen
    +static void fnRefreshSample(FontBoxStateT *fn) {
    +    wgtSetFont(fn->sample, fn->cur.family, fn->cur.size, fn->cur.style);
    +}
    +
    +
    +// The sizes of the family and style chosen, the one nearest the size
    +// chosen selected, and the sample
    +static void fnRefreshSizes(FontBoxStateT *fn) {
    +    int32_t best = 0;
    +
    +    fn->sizeCount = DVX_MIN(dvxFontSizes(fn->cur.family, fn->cur.style & FONT_STYLE_FACE, fn->sizes, DFN_STRIKE_MAX), DFN_STRIKE_MAX);
    +
    +    for (int32_t i = 0; i < fn->sizeCount; i++) {
    +        snprintf(fn->sizeText[i], sizeof(fn->sizeText[i]), "%" PRId32, fn->sizes[i]);
    +        fn->sizeItems[i] = fn->sizeText[i];
    +
    +        if (abs(fn->sizes[i] - fn->cur.size) < abs(fn->sizes[best] - fn->cur.size)) {
    +            best = i;
    +        }
    +    }
    +
    +    wgtListBoxSetItems(fn->sizeList, fn->sizeItems, fn->sizeCount);
    +
    +    if (fn->sizeCount > 0) {
    +        fn->cur.size = fn->sizes[best];
    +        wgtListBoxSetSelected(fn->sizeList, best);
    +    }
    +
    +    fnRefreshSample(fn);
    +}
    +
    +
    +// text (a font's notice and licence, CR LF lines) read-only in a box
    +// that scrolls, until Close
    +static void fnShowLicence(AppContextT *ctx, const char *family, char *text) {
    +    char     title[FN_TITLE_MAX];
    +    char    *out = text;
    +    WindowT *dlg;
    +
    +    snprintf(title, sizeof(title), "%s Licence", family);
    +    dlg = dvxDialogCreate(ctx, title, FN_LICENCE_W);
    +
    +    if (!dlg) {
    +        return;
    +    }
    +
    +    // One line end, not two characters
    +    for (const char *in = text; *in; in++) {
    +        if (*in != '\r') {
    +            *out++ = *in;
    +        }
    +    }
    +
    +    *out = '\0';
    +
    +    CustomDialogStateT *state = (CustomDialogStateT *)dlg->userData;
    +    WidgetT            *box   = wgtTextArea(dlg->widgetRoot, (int32_t)strlen(text) + 1);
    +    WidgetT            *row   = wgtHBox(dlg->widgetRoot);
    +    WidgetT            *close = row ? wgtButton(row, "&Close") : NULL;
    +
    +    if (box) {
    +        box->minW = wgtPixels(FN_LICENCE_W);
    +        box->minH = wgtPixels(FN_LICENCE_H);
    +        wgtSetText(box, text);
    +
    +        // Read-only, not disabled: disabling would stop the scroll bar too
    +        wgtSetReadOnly(box, true);
    +    }
    +
    +    if (close) {
    +        row->align             = AlignEndE;
    +        close->minW            = wgtPixels(BUTTON_WIDTH);
    +        close->minH            = wgtPixels(BUTTON_HEIGHT);
    +        close->onClick         = customOnOk;
    +        state->base.defaultBtn = close;
    +        state->base.cancelBtn  = close;
    +    }
    +
    +    dvxDialogRun(ctx, dlg);
    +}
    +
    +
     static void ibOnOk(WidgetT *w) {
         InputBoxStateT *ib = (InputBoxStateT *)w->window->userData;
     
    diff --git a/src/libs/kpunch/libdvx/dvxDlg.h b/src/libs/kpunch/libdvx/dvxDlg.h
    index 4a7bafa7..05328214 100644
    --- a/src/libs/kpunch/libdvx/dvxDlg.h
    +++ b/src/libs/kpunch/libdvx/dvxDlg.h
    @@ -187,6 +187,31 @@ bool dvxColorDialog(AppContextT *ctx, const char *title, uint32_t *rgbInOut);
     #define DVX_RGB_GREEN(rgb)   ((uint8_t)(((rgb) >> DVX_RGB_SHIFT_GREEN) & DVX_RGB_CHANNEL_MASK))
     #define DVX_RGB_BLUE(rgb)    ((uint8_t)((rgb) & DVX_RGB_CHANNEL_MASK))
     
    +// ============================================================
    +// Font dialog
    +// ============================================================
    +//
    +// Display a modal font picker: the font families (dvxText.h), the four
    +// styles, the sizes the family comes in for the chosen style, Underline
    +// and Strikeout boxes, and a sample drawn in the chosen font.
    +// *choiceInOut holds the starting font and receives the chosen one (a
    +// family there is none of starts on "System").  flags
    +// DVX_FONT_DLG_FIXED offers only fixed-pitch families (for a text editor
    +// or a terminal).  Returns true when the user chose OK, false when
    +// cancelled or closed (then *choiceInOut is unchanged).  title may be
    +// NULL ("Font").
    +
    +#define DVX_FONT_DLG_FIXED 0x0001
    +
    +// A font as the font dialog and the BASIC Font properties name it
    +typedef struct {
    +    char     family[FONT_FAMILY_MAX];
    +    int32_t  size;      // pixels
    +    uint32_t style;     // FONT_STYLE_* (bold, italic, underline, strike)
    +} DvxFontChoiceT;
    +
    +bool dvxFontDialog(AppContextT *ctx, const char *title, uint32_t flags, DvxFontChoiceT *choiceInOut);
    +
     // ============================================================
     // Custom dialogs
     // ============================================================
    diff --git a/src/libs/kpunch/libdvx/dvxDraw.c b/src/libs/kpunch/libdvx/dvxDraw.c
    index b416036b..fc661fc3 100644
    --- a/src/libs/kpunch/libdvx/dvxDraw.c
    +++ b/src/libs/kpunch/libdvx/dvxDraw.c
    @@ -96,6 +96,14 @@ typedef enum { AccelTokenEndE, AccelTokenLiteralE, AccelTokenAccelE, AccelTokenN
     // Pixel columns in one 1bpp glyph row byte: the width of a fully visible
     // cell, which the unrolled glyph-row helpers below write in one go.
     #define GLYPH_COLS        8
    +#define GRAY_CHANNELS     3        // red, green, blue (grayBlend)
    +
    +// A glyph row byte is two nibbles of 4 pixels, bit 3 the leftmost
    +#define NIBBLE_BITS       4
    +#define NIBBLE_PATTERNS   16
    +#define NIBBLE_MASK       0x0F
    +#define NIBBLE_LEFT_BIT   0x08
    +#define NIBBLE_WORDS_MAX  4        // 32-bit words of a nibble's pixels at 32 bpp
     
     // What drawFocusRect plots its dots with
     typedef struct {
    @@ -104,20 +112,48 @@ typedef struct {
         uint32_t  color;
     } FocusDotCtxT;
     
    +// Each glyph-row nibble's 4 pixels in one color pair, as the 32-bit
    +// words they are stored in: 1 at 8 bpp, 2 at 16, 4 at 32 (words[nibble]).
    +// drawTermRow writes a fully visible cell's row as two of these, made
    +// again only when a cell's colors (or the depth) differ from the last:
    +// terminal text comes in runs of one color.
    +typedef struct {
    +    uint32_t fg;
    +    uint32_t bg;
    +    int32_t  bpp;
    +    bool     valid;
    +    uint32_t words[NIBBLE_PATTERNS][NIBBLE_WORDS_MAX];
    +} NibbleTableT;
    +
     // ============================================================
     // Prototypes
     // ============================================================
     
    -static AccelTokenE accelNextToken(const char **text, char *outCh);
    -static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t *w, int32_t *h);
    -static void        focusDot(void *ctx, int32_t x, int32_t y);
    -static inline void glyphRowFg16(uint16_t *dst, uint8_t bits, uint16_t fg);
    -static inline void glyphRowFg32(uint32_t *dst, uint8_t bits, uint32_t fg);
    -static inline void glyphRowFg8(uint8_t *dst, uint8_t bits, uint8_t fg);
    -static inline void glyphRowOpaque16(uint16_t *dst, uint8_t bits, uint16_t fg, uint16_t bg);
    -static inline void glyphRowOpaque32(uint32_t *dst, uint8_t bits, uint32_t fg, uint32_t bg);
    -static inline void glyphRowOpaque8(uint8_t *dst, uint8_t bits, uint8_t fg, uint8_t bg);
    -static inline void putPixel(uint8_t *dst, uint32_t color, int32_t bpp);
    +static AccelTokenE    accelNextToken(const char **text, char *outCh);
    +static inline int32_t charAdvance(const BitmapFontT *font, uint8_t ch);
    +static inline void   clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t *w, int32_t *h);
    +static int32_t       drawCharFixed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, char ch, uint32_t fg, uint32_t bg, bool opaque);
    +static void          drawGlyph(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t idx, int32_t x, int32_t baseline, uint32_t fg);
    +static void          drawGlyphBits(DisplayT *d, const BlitOpsT *ops, const uint8_t *bits, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t fg);
    +static void          drawGlyphGray(DisplayT *d, const BlitOpsT *ops, const uint8_t *bits, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t fg);
    +static void          drawTermRowGlyphs(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, int32_t cols, const uint8_t *lineData, const uint32_t *palette, bool blinkVisible, int32_t cursorCol);
    +static void          drawTextLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, int32_t width, uint32_t lines, int32_t scale, uint32_t fg);
    +static void          drawTextNFixed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t count, uint32_t fg, uint32_t bg, bool opaque);
    +static int32_t       drawTextProp(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t count, uint32_t fg, uint32_t bg, bool opaque);
    +static void          drawTextScaledProp(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t scale, uint32_t fg);
    +static void          focusDot(void *ctx, int32_t x, int32_t y);
    +static inline void   glyphRowFg16(uint16_t *dst, uint8_t bits, uint16_t fg);
    +static inline void   glyphRowFg32(uint32_t *dst, uint8_t bits, uint32_t fg);
    +static inline void   glyphRowFg8(uint8_t *dst, uint8_t bits, uint8_t fg);
    +static inline void   glyphRowOpaque16(uint16_t *dst, uint8_t bits, uint16_t fg, uint16_t bg);
    +static inline void   glyphRowOpaque32(uint32_t *dst, uint8_t bits, uint32_t fg, uint32_t bg);
    +static uint32_t      grayBlend(const DisplayT *d, uint32_t back, uint32_t fg, int32_t level);
    +static inline void   nibbleRow16(uint8_t *dst, const NibbleTableT *t, uint8_t bits);
    +static inline void   nibbleRow32(uint8_t *dst, const NibbleTableT *t, uint8_t bits);
    +static inline void   nibbleRow8(uint8_t *dst, const NibbleTableT *t, uint8_t bits);
    +static void          nibbleTableSet(NibbleTableT *t, uint32_t fg, uint32_t bg, int32_t bpp);
    +static inline void   putPixel(uint8_t *dst, uint32_t color, int32_t bpp);
    +static void          termCellColors(uint8_t attr, const uint32_t *palette, bool blinkVisible, bool cursor, uint32_t *fg, uint32_t *bg);
     
     // Bit lookup tables for glyph and mask rendering.  On a 486, a variable
     // shift (1 << (7 - col)) costs 4 cycles per bit position; a table
    @@ -129,6 +165,9 @@ static inline void putPixel(uint8_t *dst, uint32_t color, int32_t bpp);
     static const uint8_t  sGlyphBit[GLYPH_COLS] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
     static const uint16_t sMaskBit[16]  = {0x8000, 0x4000, 0x2000, 0x1000, 0x0800, 0x0400, 0x0200, 0x0100, 0x0080, 0x0040, 0x0020, 0x0010, 0x0008, 0x0004, 0x0002, 0x0001};
     
    +// drawTermRow's patterns for the last cell colors it drew
    +static NibbleTableT sTermNibbles;
    +
     
     // ============================================================
     // accelNextToken
    @@ -251,6 +290,19 @@ void calcCenteredText(const BitmapFontT *font, int32_t rectX, int32_t rectY, int
     // branch predictor happy (static not-taken prediction for forward
     // branches), and on 486 it at least avoids the taken-branch penalty.
     
    +// The pen movement of character ch: a loaded font's glyph advance, the
    +// cell width of the system font (and of a code outside a font's range)
    +static inline int32_t charAdvance(const BitmapFontT *font, uint8_t ch) {
    +    int32_t idx = ch - font->firstChar;
    +
    +    if (!font->glyphs || idx < 0 || idx >= font->numChars) {
    +        return font->charWidth;
    +    }
    +
    +    return font->glyphs[idx].advance;
    +}
    +
    +
     static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t *w, int32_t *h) {
         int32_t cx2 = d->clipX + d->clipW;
         int32_t cy2 = d->clipY + d->clipH;
    @@ -415,6 +467,18 @@ void drawBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w
     // across a full screen of text (~6400 characters at 80x80).
     
     int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, char ch, uint32_t fg, uint32_t bg, bool opaque) {
    +    int32_t advance = font->glyphs ? drawTextProp(d, ops, font, x, y, &ch, 1, fg, bg, opaque) : drawCharFixed(d, ops, font, x, y, ch, fg, bg, opaque);
    +
    +    if (font->style & FONT_STYLE_LINES) {
    +        drawTextLines(d, ops, font, x, y, advance, font->style, 1, fg);
    +    }
    +
    +    return advance;
    +}
    +
    +
    +// drawChar in the system font (8-pixel cells, one byte per glyph row)
    +static int32_t drawCharFixed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, char ch, uint32_t fg, uint32_t bg, bool opaque) {
         int32_t cw  = font->charWidth;
         int32_t chh = font->charHeight;
     
    @@ -643,6 +707,122 @@ void drawFocusRectDots(int32_t x, int32_t y, int32_t w, int32_t h, DrawDotFnT fn
     }
     
     
    +// Glyph idx of a loaded font with its pen at x on baseline, in fg: a
    +// gray (smoothed) font's blended, a 1-bit one's set pixels
    +static void drawGlyph(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t idx, int32_t x, int32_t baseline, uint32_t fg) {
    +    const FontGlyphT *g    = &font->glyphs[idx];
    +    const uint8_t    *bits = font->glyphData + font->glyphAt[idx];
    +
    +    if (g->width == 0) {
    +        return;
    +    }
    +
    +    if (font->flags & FONT_FLAG_GRAY) {
    +        drawGlyphGray(d, ops, bits, x + g->left, baseline - g->top, g->width, g->height, fg);
    +    } else {
    +        drawGlyphBits(d, ops, bits, x + g->left, baseline - g->top, g->width, g->height, fg);
    +    }
    +}
    +
    +
    +// The set pixels of a 1-bit bitmap w x h (rows of FONT_ROW_BYTES(w)
    +// bytes, the leftmost pixel in the top bit) in fg with its top-left at
    +// x, y, clipped.  Each byte whose 8 pixels lie inside the clip rect goes
    +// through the unrolled row writers; only bytes on a clip edge are drawn
    +// pixel by pixel.  The bits past w are 0 in every bitmap DVX loads.
    +static void drawGlyphBits(DisplayT *d, const BlitOpsT *ops, const uint8_t *bits, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t fg) {
    +    int32_t clipX1 = d->clipX;
    +    int32_t clipX2 = d->clipX + d->clipW;
    +
    +    if (x + w <= clipX1 || x >= clipX2) {
    +        return;
    +    }
    +
    +    int32_t rowStart = DVX_MAX(0, d->clipY - y);
    +    int32_t rowEnd   = DVX_MIN(h, d->clipY + d->clipH - y);
    +    int32_t rowBytes = FONT_ROW_BYTES(w);
    +    int32_t bpp      = ops->bytesPerPixel;
    +
    +    for (int32_t row = rowStart; row < rowEnd; row++) {
    +        const uint8_t *src  = bits + row * rowBytes;
    +        uint8_t       *line = d->backBuf + (y + row) * d->pitch;
    +
    +        for (int32_t b = 0; b < rowBytes; b++) {
    +            uint8_t byte = src[b];
    +            int32_t px   = x + b * GLYPH_COLS;
    +
    +            if (byte == 0) {
    +                continue;
    +            }
    +
    +            if (px >= clipX1 && px + GLYPH_COLS <= clipX2) {
    +                if (bpp == 4) {
    +                    glyphRowFg32((uint32_t *)(line + px * 4), byte, fg);
    +                } else if (bpp == 2) {
    +                    glyphRowFg16((uint16_t *)(line + px * 2), byte, (uint16_t)fg);
    +                } else {
    +                    glyphRowFg8(line + px, byte, (uint8_t)fg);
    +                }
    +
    +                continue;
    +            }
    +
    +            for (int32_t col = 0; col < GLYPH_COLS; col++) {
    +                if ((byte & sGlyphBit[col]) && px + col >= clipX1 && px + col < clipX2) {
    +                    putPixel(line + (px + col) * bpp, fg, bpp);
    +                }
    +            }
    +        }
    +    }
    +}
    +
    +
    +// A gray glyph bitmap w x h (FONT_GRAY_BPP bits per pixel) at x, y in
    +// fg, clipped: full-level pixels in fg, partial ones fg blended into what
    +// is there (grayBlend); at 8 bits per pixel, with no colors to blend, the
    +// pixels of FONT_GRAY_HALF and more in fg.  Text mostly stands on one
    +// color, so each partial level's last blend is kept and used again.
    +static void drawGlyphGray(DisplayT *d, const BlitOpsT *ops, const uint8_t *bits, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t fg) {
    +    int32_t  x0       = DVX_MAX(0, d->clipX - x);
    +    int32_t  x1       = DVX_MIN(w, d->clipX + d->clipW - x);
    +    int32_t  rowStart = DVX_MAX(0, d->clipY - y);
    +    int32_t  rowEnd   = DVX_MIN(h, d->clipY + d->clipH - y);
    +    int32_t  rowBytes = FONT_ROW_BYTES_BPP(w, FONT_GRAY_BPP);
    +    int32_t  bpp      = ops->bytesPerPixel;
    +    uint32_t lastBack[FONT_GRAY_FULL];
    +    uint32_t lastOut[FONT_GRAY_FULL];
    +    bool     have[FONT_GRAY_FULL] = { false };
    +
    +    for (int32_t row = rowStart; row < rowEnd; row++) {
    +        const uint8_t *src  = bits + row * rowBytes;
    +        uint8_t       *line = d->backBuf + (y + row) * d->pitch;
    +
    +        for (int32_t col = x0; col < x1; col++) {
    +            int32_t  level = FONT_GRAY_LEVEL(src, col);
    +            uint8_t *dst   = line + (x + col) * bpp;
    +
    +            if (level == 0) {
    +                continue;
    +            }
    +
    +            if (level == FONT_GRAY_FULL || (bpp == 1 && level >= FONT_GRAY_HALF)) {
    +                putPixel(dst, fg, bpp);
    +            } else if (bpp > 1) {
    +                uint32_t back = (bpp == 2) ? *(const uint16_t *)dst : *(const uint32_t *)dst;
    +
    +                if (!have[level] || back != lastBack[level]) {
    +                    lastBack[level] = back;
    +                    lastOut[level]  = grayBlend(d, back, fg, level);
    +                    have[level]     = true;
    +                }
    +
    +                putPixel(dst, lastOut[level], bpp);
    +            }
    +        }
    +    }
    +}
    +
    +
     // ============================================================
     // drawHLine
     // ============================================================
    @@ -924,6 +1104,11 @@ void drawScrollbarArrowButton(DisplayT *d, const BlitOpsT *ops, const ColorSchem
     // inverted fg/bg to show the text cursor.
     
     void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, int32_t cols, const uint8_t *lineData, const uint32_t *palette, bool blinkVisible, int32_t cursorCol) {
    +    if (font->glyphs) {
    +        drawTermRowGlyphs(d, ops, font, x, y, cols, lineData, palette, blinkVisible, cursorCol);
    +        return;
    +    }
    +
         int32_t cw    = font->charWidth;
         int32_t ch    = font->charHeight;
         int32_t bpp   = ops->bytesPerPixel;
    @@ -972,23 +1157,11 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
     
         // Render each visible cell
         for (int32_t col = firstCol; col < lastCol; col++) {
    -        uint8_t  gch  = lineData[col * 2];
    -        uint8_t  attr = lineData[col * 2 + 1];
    -        uint32_t fg   = palette[attr & TERM_ATTR_FG_MASK];
    -        uint32_t bg   = palette[(attr >> TERM_ATTR_BG_SHIFT) & TERM_ATTR_BG_MASK];
    +        uint8_t  gch = lineData[col * 2];
    +        uint32_t fg;
    +        uint32_t bg;
     
    -        // Blink: hide text during off phase
    -        if ((attr & TERM_ATTR_BLINK) && !blinkVisible) {
    -            fg = bg;
    -        }
    -
    -        // Cursor: invert colors
    -        if (col == cursorCol) {
    -            uint32_t tmp = fg;
    -
    -            fg = bg;
    -            bg = tmp;
    -        }
    +        termCellColors(lineData[col * 2 + 1], palette, blinkVisible, col == cursorCol, &fg, &bg);
     
             int32_t cx = x + col * cw;
     
    @@ -1021,6 +1194,10 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
     
             // Render scanlines: fully visible cells use the unrolled row
             // writers, edge cells the per-pixel table loop.
    +        if (full) {
    +            nibbleTableSet(&sTermNibbles, fg, bg, bpp);
    +        }
    +
             if (bpp == 2) {
                 uint16_t fg16 = (uint16_t)fg;
                 uint16_t bg16 = (uint16_t)bg;
    @@ -1030,7 +1207,7 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
                     uint8_t   bits = glyph[row];
     
                     if (full) {
    -                    glyphRowOpaque16(dst, bits, fg16, bg16);
    +                    nibbleRow16((uint8_t *)dst, &sTermNibbles, bits);
                     } else {
                         for (int32_t p = cStart; p < cEnd; p++) {
                             dst[p] = (bits & sGlyphBit[p]) ? fg16 : bg16;
    @@ -1043,7 +1220,7 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
                     uint8_t   bits = glyph[row];
     
                     if (full) {
    -                    glyphRowOpaque32(dst, bits, fg, bg);
    +                    nibbleRow32((uint8_t *)dst, &sTermNibbles, bits);
                     } else {
                         for (int32_t p = cStart; p < cEnd; p++) {
                             dst[p] = (bits & sGlyphBit[p]) ? fg : bg;
    @@ -1059,7 +1236,7 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
                     uint8_t  bits = glyph[row];
     
                     if (full) {
    -                    glyphRowOpaque8(dst, bits, fg8, bg8);
    +                    nibbleRow8(dst, &sTermNibbles, bits);
                     } else {
                         for (int32_t p = cStart; p < cEnd; p++) {
                             dst[p] = (bits & sGlyphBit[p]) ? fg8 : bg8;
    @@ -1071,6 +1248,41 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
     }
     
     
    +// drawTermRow in a loaded (fixed-pitch) font: each visible cell's
    +// background, then its glyph's ink at the cell's pen
    +static void drawTermRowGlyphs(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, int32_t cols, const uint8_t *lineData, const uint32_t *palette, bool blinkVisible, int32_t cursorCol) {
    +    int32_t cw       = font->charWidth;
    +    int32_t clipX2   = d->clipX + d->clipW;
    +    int32_t baseline = y + font->ascent;
    +
    +    if (y + font->charHeight <= d->clipY || y >= d->clipY + d->clipH) {
    +        return;
    +    }
    +
    +    for (int32_t col = 0; col < cols; col++) {
    +        int32_t  cx  = x + col * cw;
    +        int32_t  idx = lineData[col * 2] - font->firstChar;
    +        uint32_t fg;
    +        uint32_t bg;
    +
    +        if (cx >= clipX2) {
    +            break;
    +        }
    +
    +        if (cx + cw <= d->clipX) {
    +            continue;
    +        }
    +
    +        termCellColors(lineData[col * 2 + 1], palette, blinkVisible, col == cursorCol, &fg, &bg);
    +        rectFill(d, ops, cx, y, cw, font->charHeight, bg);
    +
    +        if (idx >= 0 && idx < font->numChars) {
    +            drawGlyph(d, ops, font, idx, cx, baseline, fg);
    +        }
    +    }
    +}
    +
    +
     // ============================================================
     // drawText
     // ============================================================
    @@ -1095,12 +1307,11 @@ void drawText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t
     // draws a literal &.  This matches the Windows/Motif convention for
     // menu and button labels.
     //
    -// The underline is drawn as a 1px horizontal line at the bottom of
    -// the character cell (y + charHeight - 1), which is the standard
    -// placement for accelerator underlines.
    +// The mark is the font's underline (underlineY, lineThick: the system
    +// font's is its cell's bottom row, the standard place for accelerator
    +// marks) under the character's advance.
     
     void drawTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque) {
    -    int32_t cw     = font->charWidth;
         int32_t clipX2 = d->clipX + d->clipW;
     
         char        ch;
    @@ -1111,15 +1322,17 @@ void drawTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, in
                 break;
             }
     
    -        if (x + cw > d->clipX) {
    +        int32_t advance = charAdvance(font, (uint8_t)ch);
    +
    +        if (x + advance > d->clipX) {
                 drawChar(d, ops, font, x, y, ch, fg, bg, opaque);
     
                 if (token == AccelTokenAccelE) {
    -                drawHLine(d, ops, x, y + font->charHeight - 1, cw, fg);
    +                rectFill(d, ops, x, y + font->underlineY, advance, font->lineThick, fg);
                 }
             }
     
    -        x += cw;
    +        x += advance;
         }
     }
     
    @@ -1138,6 +1351,20 @@ void drawTextAccelEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *
     }
     
     
    +// The lines (FONT_STYLE_UNDERLINE / STRIKE) under and through a run of
    +// text width wide at x, y in fg, at the font's rows and thickness, each
    +// row scale times as far down and as thick (drawTextScaled)
    +static void drawTextLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, int32_t width, uint32_t lines, int32_t scale, uint32_t fg) {
    +    if (lines & FONT_STYLE_UNDERLINE) {
    +        rectFill(d, ops, x, y + font->underlineY * scale, width, font->lineThick * scale, fg);
    +    }
    +
    +    if (lines & FONT_STYLE_STRIKE) {
    +        rectFill(d, ops, x, y + font->strikeY * scale, width, font->lineThick * scale, fg);
    +    }
    +}
    +
    +
     // ============================================================
     // drawTextN
     // ============================================================
    @@ -1167,6 +1394,20 @@ void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_
             return;
         }
     
    +    if (font->glyphs) {
    +        drawTextProp(d, ops, font, x, y, text, count, fg, bg, opaque);
    +    } else {
    +        drawTextNFixed(d, ops, font, x, y, text, count, fg, bg, opaque);
    +    }
    +
    +    if (font->style & FONT_STYLE_LINES) {
    +        drawTextLines(d, ops, font, x, y, textWidthN(font, text, count), font->style, 1, fg);
    +    }
    +}
    +
    +
    +// drawTextN in the system font (8-pixel cells, one byte per glyph row)
    +static void drawTextNFixed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t count, uint32_t fg, uint32_t bg, bool opaque) {
         int32_t cw    = font->charWidth;
         int32_t ch    = font->charHeight;
         int32_t bpp   = ops->bytesPerPixel;
    @@ -1328,6 +1569,42 @@ void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_
     }
     
     
    +// Text in a loaded font (glyphs != NULL): an opaque run's background is
    +// filled once across the run, then each glyph's ink is drawn at the pen
    +// (its bitmap box may reach past the advance: italics).  Glyphs past the
    +// clip rect's right edge (with a character's width of slack for an
    +// overhang) end the run.  Returns the run's width (textWidthN).
    +static int32_t drawTextProp(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t count, uint32_t fg, uint32_t bg, bool opaque) {
    +    int32_t width = textWidthN(font, text, count);
    +
    +    if (count <= 0 || y + font->charHeight <= d->clipY || y >= d->clipY + d->clipH) {
    +        return width;
    +    }
    +
    +    if (opaque) {
    +        rectFill(d, ops, x, y, width, font->charHeight, bg);
    +    }
    +
    +    int32_t stop     = d->clipX + d->clipW + font->charWidth;
    +    int32_t baseline = y + font->ascent;
    +    int32_t pen      = x;
    +
    +    for (int32_t i = 0; i < count && pen < stop; i++) {
    +        int32_t idx = (uint8_t)text[i] - font->firstChar;
    +
    +        if (idx < 0 || idx >= font->numChars) {
    +            pen += font->charWidth;
    +            continue;
    +        }
    +
    +        drawGlyph(d, ops, font, idx, pen, baseline, fg);
    +        pen += font->glyphs[idx].advance;
    +    }
    +
    +    return width;
    +}
    +
    +
     // Text with every glyph pixel drawn as a scale x scale block in fg (the
     // background shows through): titles and banners in the 8x16 font.  Each
     // run of set pixels in a glyph row is one rectFill, which clips; a glyph
    @@ -1342,6 +1619,15 @@ void drawTextScaled(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, i
             return;
         }
     
    +    if (font->style & FONT_STYLE_LINES) {
    +        drawTextLines(d, ops, font, x, y, textWidth(font, text) * scale, font->style, scale, fg);
    +    }
    +
    +    if (font->glyphs) {
    +        drawTextScaledProp(d, ops, font, x, y, text, scale, fg);
    +        return;
    +    }
    +
         int32_t cellW = font->charWidth * scale;
         int32_t cellH = font->charHeight * scale;
     
    @@ -1380,6 +1666,73 @@ void drawTextScaled(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, i
     }
     
     
    +// drawTextScaled in a loaded font: each run of set pixels in a glyph
    +// row one rectFill, the pen moving by scale times each advance (a gray
    +// glyph's pixels of FONT_GRAY_HALF and more, unblended).
    +static void drawTextScaledProp(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t scale, uint32_t fg) {
    +    if (y + font->charHeight * scale <= d->clipY || y >= d->clipY + d->clipH) {
    +        return;
    +    }
    +
    +    for (; *text; text++) {
    +        int32_t idx = (uint8_t)*text - font->firstChar;
    +
    +        if (idx < 0 || idx >= font->numChars) {
    +            x += font->charWidth * scale;
    +            continue;
    +        }
    +
    +        const FontGlyphT *g        = &font->glyphs[idx];
    +        const uint8_t    *bits     = font->glyphData + font->glyphAt[idx];
    +        bool              gray     = (font->flags & FONT_FLAG_GRAY) != 0;
    +        int32_t           rowBytes = FONT_ROW_BYTES_BPP(g->width, FONT_BPP(font));
    +        int32_t           gx       = x + g->left * scale;
    +        int32_t           gy       = y + (font->ascent - g->top) * scale;
    +
    +        for (int32_t row = 0; row < g->height; row++) {
    +            const uint8_t *src = bits + row * rowBytes;
    +            int32_t        col = 0;
    +
    +            while (col < g->width) {
    +                int32_t end = col;
    +
    +                while (end < g->width && (gray ? FONT_GRAY_LEVEL(src, end) >= FONT_GRAY_HALF : (src[FONT_PIXEL_BYTE(end)] & FONT_PIXEL_BIT(end)) != 0)) {
    +                    end++;
    +                }
    +
    +                if (end > col) {
    +                    rectFill(d, ops, gx + col * scale, gy + row * scale, (end - col) * scale, scale, fg);
    +                    col = end;
    +                } else {
    +                    col++;
    +                }
    +            }
    +        }
    +
    +        x += g->advance * scale;
    +    }
    +}
    +
    +
    +// Text with the lines style asks for (FONT_STYLE_UNDERLINE / STRIKE) at
    +// the font's rows, besides those the font draws itself; bold and italic
    +// are the font's own (dvxFontOpen).
    +void drawTextStyled(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t style, uint32_t fg, uint32_t bg, bool opaque) {
    +    drawTextStyledN(d, ops, font, x, y, text, (int32_t)strlen(text), style, fg, bg, opaque);
    +}
    +
    +
    +void drawTextStyledN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t count, uint32_t style, uint32_t fg, uint32_t bg, bool opaque) {
    +    uint32_t lines = style & FONT_STYLE_LINES & ~font->style;
    +
    +    drawTextN(d, ops, font, x, y, text, count, fg, bg, opaque);
    +
    +    if (lines && count > 0) {
    +        drawTextLines(d, ops, font, x, y, textWidthN(font, text, count), lines, 1, fg);
    +    }
    +}
    +
    +
     // ============================================================
     // drawVLine
     // ============================================================
    @@ -1450,16 +1803,6 @@ void drawWidgetTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo
     }
     
     
    -// ============================================================
    -// glyphRow helpers
    -// ============================================================
    -//
    -// One fully visible GLYPH_COLS-pixel glyph row, fully unrolled with
    -// literal bit masks: no sGlyphBit[] table lookup, loop counter or loop
    -// branch (~3 cycles per pixel saved on a 486).  The Fg variants write
    -// only the set pixels (transparent text); the Opaque variants write every
    -// pixel as fg or bg.  Shared by drawChar, drawTextN and drawTermRow.
    -
     // One dot of a focus rectangle in the back buffer, inside the clip rect.
     // cppcheck-suppress constParameterCallback ; a DrawDotFnT: the context stays a plain void * for callers that keep state
     static void focusDot(void *ctx, int32_t x, int32_t y) {
    @@ -1472,6 +1815,15 @@ static void focusDot(void *ctx, int32_t x, int32_t y) {
     }
     
     
    +// ============================================================
    +// glyphRow helpers
    +// ============================================================
    +//
    +// One fully visible GLYPH_COLS-pixel glyph row, fully unrolled with
    +// literal bit masks: no sGlyphBit[] table lookup, loop counter or loop
    +// branch (~3 cycles per pixel saved on a 486).  The Fg variants write
    +// only the set pixels (transparent text); the Opaque variants write every
    +// pixel as fg or bg.  Shared by drawChar, drawTextN and drawTermRow.
     static inline void glyphRowFg16(uint16_t *dst, uint8_t bits, uint16_t fg) {
         if (bits & 0x80) {
             dst[0] = fg;
    @@ -1601,15 +1953,68 @@ static inline void glyphRowOpaque32(uint32_t *dst, uint8_t bits, uint32_t fg, ui
     }
     
     
    -static inline void glyphRowOpaque8(uint8_t *dst, uint8_t bits, uint8_t fg, uint8_t bg) {
    -    dst[0] = (bits & 0x80) ? fg : bg;
    -    dst[1] = (bits & 0x40) ? fg : bg;
    -    dst[2] = (bits & 0x20) ? fg : bg;
    -    dst[3] = (bits & 0x10) ? fg : bg;
    -    dst[4] = (bits & 0x08) ? fg : bg;
    -    dst[5] = (bits & 0x04) ? fg : bg;
    -    dst[6] = (bits & 0x02) ? fg : bg;
    -    dst[7] = (bits & 0x01) ? fg : bg;
    +// fg level / FONT_GRAY_FULL of the way over back, channel by channel in
    +// the display's pixel format (bits outside the channels: fg's)
    +static uint32_t grayBlend(const DisplayT *d, uint32_t back, uint32_t fg, int32_t level) {
    +    const PixelFormatT *pf     = &d->format;
    +    const uint32_t      masks[GRAY_CHANNELS]  = { pf->redMask, pf->greenMask, pf->blueMask };
    +    const int32_t       shifts[GRAY_CHANNELS] = { pf->redShift, pf->greenShift, pf->blueShift };
    +    uint32_t            out    = fg & ~(pf->redMask | pf->greenMask | pf->blueMask);
    +
    +    for (int32_t i = 0; i < GRAY_CHANNELS; i++) {
    +        uint32_t f = (fg & masks[i]) >> shifts[i];
    +        uint32_t b = (back & masks[i]) >> shifts[i];
    +        uint32_t c = (f * (uint32_t)level + b * (uint32_t)(FONT_GRAY_FULL - level)) / FONT_GRAY_FULL;
    +
    +        out |= (c << shifts[i]) & masks[i];
    +    }
    +
    +    return out;
    +}
    +
    +
    +// One glyph row of a fully visible cell from t (nibbleTableSet): the
    +// left nibble's pixels, then the right's.  dst is the cell's first pixel,
    +// at any alignment (constant-size copies: plain moves, no call).
    +static inline void nibbleRow16(uint8_t *dst, const NibbleTableT *t, uint8_t bits) {
    +    memcpy(dst, t->words[bits >> NIBBLE_BITS], NIBBLE_BITS * sizeof(uint16_t));
    +    memcpy(dst + NIBBLE_BITS * sizeof(uint16_t), t->words[bits & NIBBLE_MASK], NIBBLE_BITS * sizeof(uint16_t));
    +}
    +
    +
    +static inline void nibbleRow32(uint8_t *dst, const NibbleTableT *t, uint8_t bits) {
    +    memcpy(dst, t->words[bits >> NIBBLE_BITS], NIBBLE_BITS * sizeof(uint32_t));
    +    memcpy(dst + NIBBLE_BITS * sizeof(uint32_t), t->words[bits & NIBBLE_MASK], NIBBLE_BITS * sizeof(uint32_t));
    +}
    +
    +
    +static inline void nibbleRow8(uint8_t *dst, const NibbleTableT *t, uint8_t bits) {
    +    memcpy(dst, t->words[bits >> NIBBLE_BITS], NIBBLE_BITS);
    +    memcpy(dst + NIBBLE_BITS, t->words[bits & NIBBLE_MASK], NIBBLE_BITS);
    +}
    +
    +
    +// t's patterns for fg on bg at bpp bytes a pixel (kept when they already
    +// are): each nibble's pixels written as putPixel writes them
    +static void nibbleTableSet(NibbleTableT *t, uint32_t fg, uint32_t bg, int32_t bpp) {
    +    if (t->valid && t->fg == fg && t->bg == bg && t->bpp == bpp) {
    +        return;
    +    }
    +
    +    for (int32_t n = 0; n < NIBBLE_PATTERNS; n++) {
    +        uint8_t px[NIBBLE_WORDS_MAX * sizeof(uint32_t)];
    +
    +        for (int32_t p = 0; p < NIBBLE_BITS; p++) {
    +            putPixel(px + p * bpp, ((n << p) & NIBBLE_LEFT_BIT) ? fg : bg, bpp);
    +        }
    +
    +        memcpy(t->words[n], px, (size_t)(NIBBLE_BITS * bpp));
    +    }
    +
    +    t->fg    = fg;
    +    t->bg    = bg;
    +    t->bpp   = bpp;
    +    t->valid = true;
     }
     
     
    @@ -1617,8 +2022,8 @@ static inline void glyphRowOpaque8(uint8_t *dst, uint8_t bits, uint8_t fg, uint8
     // putPixel
     // ============================================================
     //
    -// Writes a single pixel at an already-computed buffer address.
    -// Only used by drawFocusRect for its alternating dot pattern.
    +// Writes a single pixel at an already-computed buffer address, for
    +// drawFocusRect's alternating dot pattern and nibbleTableSet.
     // Marked static inline so it compiles to a direct store at the
     // call site with no function call overhead.  The bpp chain here
     // is acceptable because focus rect drawing is infrequent.
    @@ -1897,25 +2302,36 @@ void rectFill(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w,
     }
     
     
    +// The colors of a terminal cell of attribute attr: palette foreground
    +// and background, the foreground hidden in a blink's off phase, the two
    +// swapped under the cursor
    +static void termCellColors(uint8_t attr, const uint32_t *palette, bool blinkVisible, bool cursor, uint32_t *fg, uint32_t *bg) {
    +    *fg = palette[attr & TERM_ATTR_FG_MASK];
    +    *bg = palette[(attr >> TERM_ATTR_BG_SHIFT) & TERM_ATTR_BG_MASK];
    +
    +    if ((attr & TERM_ATTR_BLINK) && !blinkVisible) {
    +        *fg = *bg;
    +    }
    +
    +    if (cursor) {
    +        uint32_t tmp = *fg;
    +
    +        *fg = *bg;
    +        *bg = tmp;
    +    }
    +}
    +
    +
     // ============================================================
     // textWidth
     // ============================================================
     //
    -// Returns the pixel width of a null-terminated string.  Because all
    -// fonts are fixed-width, this is just strlen * charWidth  -- but we
    -// iterate manually rather than calling strlen to avoid a second pass
    -// over the string.  This is used heavily for layout calculations
    +// Returns the pixel width of a null-terminated string (textWidthN over
    +// its length: for the system font a multiply).  Used heavily for layout
     // (centering text in buttons, sizing menu popups, etc.).
     
     int32_t textWidth(const BitmapFontT *font, const char *text) {
    -    int32_t w = 0;
    -
    -    while (*text) {
    -        w += font->charWidth;
    -        text++;
    -    }
    -
    -    return w;
    +    return textWidthN(font, text, (int32_t)strlen(text));
     }
     
     
    @@ -1935,7 +2351,27 @@ int32_t textWidthAccel(const BitmapFontT *font, const char *text) {
         char ch;
     
         while (accelNextToken(&text, &ch) != AccelTokenEndE) {
    -        w += font->charWidth;
    +        w += charAdvance(font, (uint8_t)ch);
    +    }
    +
    +    return w;
    +}
    +
    +
    +// The width of count characters of text (0 for none)
    +int32_t textWidthN(const BitmapFontT *font, const char *text, int32_t count) {
    +    if (count <= 0) {
    +        return 0;
    +    }
    +
    +    if (!font->glyphs) {
    +        return count * font->charWidth;
    +    }
    +
    +    int32_t w = 0;
    +
    +    for (int32_t i = 0; i < count; i++) {
    +        w += charAdvance(font, (uint8_t)text[i]);
         }
     
         return w;
    diff --git a/src/libs/kpunch/libdvx/dvxDraw.h b/src/libs/kpunch/libdvx/dvxDraw.h
    index 3efeb118..472e067c 100644
    --- a/src/libs/kpunch/libdvx/dvxDraw.h
    +++ b/src/libs/kpunch/libdvx/dvxDraw.h
    @@ -71,7 +71,8 @@ void drawBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w
     
     // Draw a single character glyph. When opaque is true, the background color
     // fills the entire cell; when false, only foreground pixels are drawn
    -// (transparent background). Returns the advance width (always charWidth).
    +// (transparent background). Returns the advance width (charWidth for the
    +// system font, the glyph's advance for a loaded font).
     int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, char ch, uint32_t fg, uint32_t bg, bool opaque);
     
     // Draw a null-terminated string (drawTextN over its whole length).
    @@ -88,9 +89,22 @@ void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_
     // titles and banners.  Scale 1 is drawText; below 1 draws nothing.
     void drawTextScaled(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t scale, uint32_t fg);
     
    -// Returns the pixel width of a null-terminated string (count * charWidth).
    +// Returns the pixel width of a null-terminated string.
     int32_t textWidth(const BitmapFontT *font, const char *text);
     
    +// Returns the pixel width of the first count characters of text (0 when
    +// count is 0 or less).
    +int32_t textWidthN(const BitmapFontT *font, const char *text, int32_t count);
    +
    +// Draw text with the lines style asks for: FONT_STYLE_UNDERLINE and
    +// FONT_STYLE_STRIKE at the font's underlineY / strikeY rows, as wide as
    +// the text.  Bold and italic are the font's own (dvxFontOpen); those bits
    +// of style are ignored here.
    +void drawTextStyled(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t style, uint32_t fg, uint32_t bg, bool opaque);
    +
    +// drawTextStyled for count characters of text.
    +void drawTextStyledN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t count, uint32_t style, uint32_t fg, uint32_t bg, bool opaque);
    +
     // Scan text for an & prefix and return the following character as a
     // lowercase accelerator key. "&File" -> 'f', "E&xit" -> 'x'.
     // Used by menu and button construction to extract keyboard shortcuts.
    diff --git a/src/libs/kpunch/libdvx/dvxFlow.c b/src/libs/kpunch/libdvx/dvxFlow.c
    new file mode 100644
    index 00000000..7350abcf
    --- /dev/null
    +++ b/src/libs/kpunch/libdvx/dvxFlow.c
    @@ -0,0 +1,768 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// dvxFlow.c  -- flowing text (dvxFlow.h)
    +//
    +// The flow keeps what was added as items: text runs, breaks and paragraph
    +// starts.  Layout turns them into lines of segments -- a segment is part
    +// of one run on one line -- greedily: a piece of text (a word and the
    +// spaces after it) goes on the line when its word fits, else the line
    +// ends and the piece starts the next one, dropping its leading spaces.  A
    +// word wider than a whole line is broken where the line is full.  A
    +// line's trailing spaces neither count toward its width (alignment) nor
    +// draw (an underline would show them).  A word may go on across runs (a
    +// style changed inside it, punctuation in a run of its own): it wraps as
    +// one word, measured to its end in the runs after.
    +
    +#include "dvxFlow.h"
    +#include "dvxDlg.h"
    +#include "dvxDraw.h"
    +#include "dvxText.h"
    +#include "dvxVideo.h"
    +#include "stb_ds_wrap.h"
    +
    +#include 
    +#include 
    +
    +#define CHAR_SPACE ' '
    +
    +// What was added to a flow
    +typedef enum {
    +    FlowItemTextE,
    +    FlowItemBreakE,
    +    FlowItemParagraphE
    +} FlowItemE;
    +
    +typedef struct {
    +    FlowItemE    kind;
    +    BitmapFontT *font;      // text: held
    +    uint32_t     serial;    // text: the font's serial when last laid out
    +    int32_t      offset;    // text: its first character's offset in the flow's text
    +    uint32_t     color;     // text: 0x00RRGGBB or FLOW_COLOR_DEFAULT
    +    char        *text;      // text: owned (malloc)
    +    int32_t      len;
    +    int32_t      align;     // paragraph: FLOW_ALIGN_*
    +} FlowItemT;
    +
    +// Part of one text run on one line
    +typedef struct {
    +    int32_t item;
    +    int32_t start;
    +    int32_t len;
    +    int32_t x;              // from the line's left edge before alignment
    +} FlowSegT;
    +
    +typedef struct {
    +    int32_t firstSeg;
    +    int32_t segCount;
    +    int32_t y;
    +    int32_t ascent;
    +    int32_t height;
    +    int32_t width;          // without trailing spaces
    +    int32_t align;
    +    int32_t textStart;      // its first character's offset (the next line's is its end)
    +} FlowLineT;
    +
    +struct TextFlowT {
    +    FlowItemT *items;       // stb_ds
    +    FlowSegT  *segs;        // stb_ds; the last layout's
    +    FlowLineT *lines;       // stb_ds
    +    int32_t    textLen;     // the characters of all the text added
    +    bool       laidOut;
    +    int32_t    layoutW;
    +    int32_t    height;
    +    int32_t    widest;
    +};
    +
    +// The line being built
    +typedef struct {
    +    int32_t            x;           // pen, trailing spaces included
    +    int32_t            contentW;    // pen without trailing spaces
    +    int32_t            ascent;
    +    int32_t            descent;
    +    int32_t            firstSeg;
    +    int32_t            align;
    +    int32_t            y;
    +    int32_t            lineStart;   // the offset the line starts at
    +    int32_t            textPos;     // the offset of the next character to lay out
    +    bool               wrapped;     // started by a wrap: leading spaces dropped
    +    const BitmapFontT *font;        // the last font placed (an empty line's height)
    +} FlowLineBuildT;
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +static bool    addItem(TextFlowT *flow, const FlowItemT *item);
    +static int32_t charWidth(const TextFlowT *flow, int32_t offset, int32_t *item);
    +bool           dvxFlowAddBreak(TextFlowT *flow);
    +bool           dvxFlowAddParagraph(TextFlowT *flow, int32_t align);
    +bool           dvxFlowAddText(TextFlowT *flow, const char *text, int32_t len, BitmapFontT *font, uint32_t color);
    +void           dvxFlowCaret(const TextFlowT *flow, int32_t offset, int32_t *x, int32_t *y, int32_t *h);
    +void           dvxFlowClear(TextFlowT *flow);
    +TextFlowT     *dvxFlowCreate(void);
    +void           dvxFlowDestroy(TextFlowT *flow);
    +int32_t        dvxFlowHeight(const TextFlowT *flow);
    +int32_t        dvxFlowHitTest(const TextFlowT *flow, int32_t x, int32_t y);
    +int32_t        dvxFlowLayout(TextFlowT *flow, int32_t width);
    +void           dvxFlowLineBox(const TextFlowT *flow, int32_t line, int32_t *y, int32_t *h);
    +int32_t        dvxFlowLineCount(const TextFlowT *flow);
    +int32_t        dvxFlowLineOf(const TextFlowT *flow, int32_t offset);
    +int32_t        dvxFlowLineStart(const TextFlowT *flow, int32_t line);
    +void           dvxFlowPaint(const TextFlowT *flow, DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, uint32_t defaultFg);
    +void           dvxFlowPaintSel(const TextFlowT *flow, DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, uint32_t defaultFg, int32_t selStart, int32_t selEnd, uint32_t selFg, uint32_t selBg);
    +int32_t        dvxFlowTextLength(const TextFlowT *flow);
    +int32_t        dvxFlowWidth(const TextFlowT *flow);
    +static int32_t fitChars(const BitmapFontT *font, const char *text, int32_t len, int32_t room);
    +static bool    fontsRebuilt(const TextFlowT *flow);
    +static int32_t itemAt(const TextFlowT *flow, int32_t offset, int32_t from);
    +static void    layoutText(TextFlowT *flow, FlowLineBuildT *lb, int32_t item, int32_t width);
    +static int32_t lineAlignX(const TextFlowT *flow, const FlowLineT *line);
    +static void    lineEnd(TextFlowT *flow, FlowLineBuildT *lb, bool wrapped);
    +static int32_t lineEndOffset(const TextFlowT *flow, int32_t line);
    +static void    linePlace(TextFlowT *flow, FlowLineBuildT *lb, int32_t item, int32_t start, int32_t len, int32_t width, int32_t contentW);
    +static int32_t offsetX(const TextFlowT *flow, const FlowLineT *line, int32_t offset);
    +static void    paintPart(const TextFlowT *flow, DisplayT *d, const BlitOpsT *ops, const FlowSegT *seg, int32_t from, int32_t to, int32_t x, int32_t top, const FlowLineT *line, uint32_t fg);
    +static int32_t wordTailWidth(const TextFlowT *flow, int32_t item);
    +
    +
    +// item at the flow's end; the layout is stale
    +static bool addItem(TextFlowT *flow, const FlowItemT *item) {
    +    if (!flow) {
    +        return false;
    +    }
    +
    +    FlowItemT added = *item;
    +
    +    added.offset   = flow->textLen;
    +    flow->textLen += (added.kind == FlowItemTextE) ? added.len : 0;
    +    arrput(flow->items, added);
    +    flow->laidOut = false;
    +    return true;
    +}
    +
    +
    +// The width of the character at offset in its run's font; *item is where
    +// to start looking for its run (and becomes its run).  0 past the text.
    +static int32_t charWidth(const TextFlowT *flow, int32_t offset, int32_t *item) {
    +    int32_t i = itemAt(flow, offset, *item);
    +
    +    if (i < 0) {
    +        return 0;
    +    }
    +
    +    *item = i;
    +    return textWidthN(flow->items[i].font, flow->items[i].text + (offset - flow->items[i].offset), 1);
    +}
    +
    +
    +bool dvxFlowAddBreak(TextFlowT *flow) {
    +    FlowItemT item;
    +
    +    memset(&item, 0, sizeof(item));
    +    item.kind = FlowItemBreakE;
    +    return addItem(flow, &item);
    +}
    +
    +
    +bool dvxFlowAddParagraph(TextFlowT *flow, int32_t align) {
    +    FlowItemT item;
    +
    +    memset(&item, 0, sizeof(item));
    +    item.kind  = FlowItemParagraphE;
    +    item.align = align;
    +    return addItem(flow, &item);
    +}
    +
    +
    +bool dvxFlowAddText(TextFlowT *flow, const char *text, int32_t len, BitmapFontT *font, uint32_t color) {
    +    FlowItemT item;
    +
    +    if (!flow || !text || !font) {
    +        return false;
    +    }
    +
    +    memset(&item, 0, sizeof(item));
    +    item.kind  = FlowItemTextE;
    +    item.len   = (len < 0) ? (int32_t)strlen(text) : len;
    +    item.text  = (char *)malloc((size_t)item.len + 1);
    +    item.color = color;
    +
    +    if (!item.text) {
    +        return false;
    +    }
    +
    +    memcpy(item.text, text, (size_t)item.len);
    +    item.text[item.len] = '\0';
    +    item.font           = dvxFontRetain(font);
    +    return addItem(flow, &item);
    +}
    +
    +
    +// The caret before offset of the laid-out flow: its x, and its line's top
    +// and height (0s for a flow with no lines)
    +void dvxFlowCaret(const TextFlowT *flow, int32_t offset, int32_t *x, int32_t *y, int32_t *h) {
    +    int32_t line = dvxFlowLineOf(flow, offset);
    +
    +    *x = 0;
    +    *y = 0;
    +    *h = 0;
    +
    +    if (line < 0) {
    +        return;
    +    }
    +
    +    const FlowLineT *l = &flow->lines[line];
    +
    +    *x = offsetX(flow, l, offset);
    +    *y = l->y;
    +    *h = l->height;
    +}
    +
    +
    +void dvxFlowClear(TextFlowT *flow) {
    +    if (!flow) {
    +        return;
    +    }
    +
    +    for (int32_t i = 0; i < arrlen(flow->items); i++) {
    +        free(flow->items[i].text);
    +        dvxFontClose(flow->items[i].font);
    +    }
    +
    +    arrsetlen(flow->items, 0);
    +    arrsetlen(flow->segs, 0);
    +    arrsetlen(flow->lines, 0);
    +    flow->textLen = 0;
    +    flow->laidOut = false;
    +    flow->height  = 0;
    +    flow->widest  = 0;
    +}
    +
    +
    +TextFlowT *dvxFlowCreate(void) {
    +    return (TextFlowT *)calloc(1, sizeof(TextFlowT));
    +}
    +
    +
    +void dvxFlowDestroy(TextFlowT *flow) {
    +    if (!flow) {
    +        return;
    +    }
    +
    +    dvxFlowClear(flow);
    +    arrfree(flow->items);
    +    arrfree(flow->segs);
    +    arrfree(flow->lines);
    +    free(flow);
    +}
    +
    +
    +int32_t dvxFlowHeight(const TextFlowT *flow) {
    +    return flow ? flow->height : 0;
    +}
    +
    +
    +// The offset nearest x, y of the laid-out flow (from its top-left as
    +// painted): on the line at y (the first or last line beyond them), the
    +// character boundary nearest x.  A wrapped line's end is its last space
    +// or, broken inside a word, the next line's start.
    +int32_t dvxFlowHitTest(const TextFlowT *flow, int32_t x, int32_t y) {
    +    int32_t count = dvxFlowLineCount(flow);
    +    int32_t line  = 0;
    +
    +    if (count == 0) {
    +        return 0;
    +    }
    +
    +    while (line < count - 1 && y >= flow->lines[line].y + flow->lines[line].height) {
    +        line++;
    +    }
    +
    +    const FlowLineT *l     = &flow->lines[line];
    +    int32_t          limit = lineEndOffset(flow, line);
    +    int32_t          item  = (limit > l->textStart) ? itemAt(flow, limit - 1, 0) : -1;
    +    int32_t          cur   = offsetX(flow, l, l->textStart);
    +
    +    // Before the next line's start: a wrapped line ending in a space
    +    if (line < count - 1 && item >= 0 && flow->items[item].text[limit - 1 - flow->items[item].offset] == CHAR_SPACE) {
    +        limit--;
    +    }
    +
    +    item = 0;
    +
    +    for (int32_t o = l->textStart; o < limit; o++) {
    +        int32_t w = charWidth(flow, o, &item);
    +
    +        if (x < cur + w / 2) {
    +            return o;
    +        }
    +
    +        cur += w;
    +    }
    +
    +    return limit;
    +}
    +
    +
    +int32_t dvxFlowLayout(TextFlowT *flow, int32_t width) {
    +    FlowLineBuildT lb;
    +
    +    if (!flow) {
    +        return 0;
    +    }
    +
    +    if (flow->laidOut && flow->layoutW == width && !fontsRebuilt(flow)) {
    +        return flow->height;
    +    }
    +
    +    memset(&lb, 0, sizeof(lb));
    +    lb.align = FLOW_ALIGN_LEFT;
    +    arrsetlen(flow->segs, 0);
    +    arrsetlen(flow->lines, 0);
    +    flow->widest = 0;
    +
    +    for (int32_t i = 0; i < arrlen(flow->items); i++) {
    +        FlowItemT *item = &flow->items[i];
    +
    +        if (item->kind == FlowItemTextE) {
    +            item->serial = item->font->serial;
    +            layoutText(flow, &lb, i, width);
    +        } else if (item->kind == FlowItemBreakE) {
    +            lineEnd(flow, &lb, false);
    +        } else {
    +            // A paragraph ends the line it follows when that has text
    +            if (arrlen(flow->segs) > lb.firstSeg) {
    +                lineEnd(flow, &lb, false);
    +            }
    +
    +            lb.align = item->align;
    +        }
    +    }
    +
    +    // The last line, when it has text (a break at the very end adds none);
    +    // text that is all empty is one empty line in its first font
    +    if (arrlen(flow->segs) > lb.firstSeg) {
    +        lineEnd(flow, &lb, false);
    +    } else if (arrlen(flow->lines) == 0) {
    +        for (int32_t i = 0; i < arrlen(flow->items) && !lb.font; i++) {
    +            lb.font = (flow->items[i].kind == FlowItemTextE) ? flow->items[i].font : NULL;
    +        }
    +
    +        if (lb.font) {
    +            lineEnd(flow, &lb, false);
    +        }
    +    }
    +
    +    flow->laidOut = true;
    +    flow->layoutW = width;
    +    flow->height  = lb.y;
    +    return flow->height;
    +}
    +
    +
    +// The top and height of line of the laid-out flow
    +void dvxFlowLineBox(const TextFlowT *flow, int32_t line, int32_t *y, int32_t *h) {
    +    bool ok = line >= 0 && line < dvxFlowLineCount(flow);
    +
    +    *y = ok ? flow->lines[line].y : 0;
    +    *h = ok ? flow->lines[line].height : 0;
    +}
    +
    +
    +int32_t dvxFlowLineCount(const TextFlowT *flow) {
    +    return (flow && flow->laidOut) ? (int32_t)arrlen(flow->lines) : 0;
    +}
    +
    +
    +// The line of the laid-out flow the caret before offset is on (a line's
    +// end is the next line's start); -1 for a flow with no lines
    +int32_t dvxFlowLineOf(const TextFlowT *flow, int32_t offset) {
    +    int32_t lo = 0;
    +    int32_t hi = dvxFlowLineCount(flow) - 1;
    +
    +    // The last line whose start is at or before offset
    +    while (lo < hi) {
    +        int32_t mid = (lo + hi + 1) / 2;
    +
    +        if (flow->lines[mid].textStart <= offset) {
    +            lo = mid;
    +        } else {
    +            hi = mid - 1;
    +        }
    +    }
    +
    +    return hi < 0 ? -1 : lo;
    +}
    +
    +
    +// The offset line of the laid-out flow starts at; past the last line, the
    +// text's length
    +int32_t dvxFlowLineStart(const TextFlowT *flow, int32_t line) {
    +    if (line < 0 || line >= dvxFlowLineCount(flow)) {
    +        return flow ? flow->textLen : 0;
    +    }
    +
    +    return flow->lines[line].textStart;
    +}
    +
    +
    +void dvxFlowPaint(const TextFlowT *flow, DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, uint32_t defaultFg) {
    +    dvxFlowPaintSel(flow, d, ops, x, y, defaultFg, 0, 0, 0, 0);
    +}
    +
    +
    +// The lines that meet the clip rect; offsets selStart to selEnd on
    +// selBg in selFg
    +void dvxFlowPaintSel(const TextFlowT *flow, DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, uint32_t defaultFg, int32_t selStart, int32_t selEnd, uint32_t selFg, uint32_t selBg) {
    +    if (!flow || !flow->laidOut) {
    +        return;
    +    }
    +
    +    for (int32_t l = 0; l < arrlen(flow->lines); l++) {
    +        const FlowLineT *line = &flow->lines[l];
    +        int32_t          top  = y + line->y;
    +        int32_t          a    = DVX_MAX(selStart, line->textStart);
    +        int32_t          b    = DVX_MIN(selEnd, lineEndOffset(flow, l));
    +
    +        if (top >= d->clipY + d->clipH) {
    +            break;
    +        }
    +
    +        if (top + line->height <= d->clipY) {
    +            continue;
    +        }
    +
    +        if (a < b) {
    +            int32_t x0 = offsetX(flow, line, a);
    +
    +            rectFill(d, ops, x + x0, top, offsetX(flow, line, b) - x0, line->height, selBg);
    +        }
    +
    +        for (int32_t s = line->firstSeg; s < line->firstSeg + line->segCount; s++) {
    +            const FlowSegT  *seg   = &flow->segs[s];
    +            const FlowItemT *item  = &flow->items[seg->item];
    +            int32_t          start = item->offset + seg->start;
    +            int32_t          end   = start + seg->len;
    +            uint32_t         fg    = defaultFg;
    +
    +            if (item->color != FLOW_COLOR_DEFAULT) {
    +                fg = packColor(d, DVX_RGB_RED(item->color), DVX_RGB_GREEN(item->color), DVX_RGB_BLUE(item->color));
    +            }
    +
    +            // Before, in and after the selection
    +            int32_t inA = DVX_MIN(DVX_MAX(selStart, start), end);
    +            int32_t inB = DVX_MAX(DVX_MIN(selEnd, end), inA);
    +
    +            paintPart(flow, d, ops, seg, start, inA, x, top, line, fg);
    +            paintPart(flow, d, ops, seg, inA, inB, x, top, line, selFg);
    +            paintPart(flow, d, ops, seg, inB, end, x, top, line, fg);
    +        }
    +    }
    +}
    +
    +
    +int32_t dvxFlowTextLength(const TextFlowT *flow) {
    +    return flow ? flow->textLen : 0;
    +}
    +
    +
    +int32_t dvxFlowWidth(const TextFlowT *flow) {
    +    return flow ? flow->widest : 0;
    +}
    +
    +
    +// How many of text's characters fit in room pixels, at least one (a line
    +// always takes something)
    +static int32_t fitChars(const BitmapFontT *font, const char *text, int32_t len, int32_t room) {
    +    int32_t count = 1;
    +    int32_t used  = textWidthN(font, text, 1);
    +
    +    while (count < len) {
    +        int32_t next = textWidthN(font, text + count, 1);
    +
    +        if (used + next > room) {
    +            break;
    +        }
    +
    +        used += next;
    +        count++;
    +    }
    +
    +    return count;
    +}
    +
    +
    +// Whether a run's font was built again (dvxFontSetSmooth) since the
    +// flow was laid out: its measurements are stale
    +static bool fontsRebuilt(const TextFlowT *flow) {
    +    for (int32_t i = 0; i < arrlen(flow->items); i++) {
    +        if (flow->items[i].kind == FlowItemTextE && flow->items[i].serial != flow->items[i].font->serial) {
    +            return true;
    +        }
    +    }
    +
    +    return false;
    +}
    +
    +
    +// The text run holding the character at offset, looking from run from on;
    +// -1 past the text
    +static int32_t itemAt(const TextFlowT *flow, int32_t offset, int32_t from) {
    +    for (int32_t i = DVX_MAX(from, 0); i < arrlen(flow->items); i++) {
    +        const FlowItemT *it = &flow->items[i];
    +
    +        if (it->kind == FlowItemTextE && offset >= it->offset && offset < it->offset + it->len) {
    +            return i;
    +        }
    +    }
    +
    +    return -1;
    +}
    +
    +
    +// Lay text run item out on the lines, wrapping at width
    +static void layoutText(TextFlowT *flow, FlowLineBuildT *lb, int32_t item, int32_t width) {
    +    const FlowItemT   *it   = &flow->items[item];
    +    const BitmapFontT *font = it->font;
    +    const char        *t    = it->text;
    +    int32_t            pos  = 0;
    +
    +    while (pos < it->len) {
    +        // A wrapped line starts at its first word: the spaces before it end
    +        // the line before
    +        if (lb->wrapped) {
    +            while (pos < it->len && t[pos] == CHAR_SPACE) {
    +                pos++;
    +            }
    +
    +            lb->textPos   = it->offset + pos;
    +            lb->lineStart = lb->textPos;
    +
    +            if (pos == it->len) {
    +                return;
    +            }
    +        }
    +
    +        // The next piece: a word and the spaces after it
    +        int32_t wordEnd = pos;
    +
    +        while (wordEnd < it->len && t[wordEnd] != CHAR_SPACE) {
    +            wordEnd++;
    +        }
    +
    +        int32_t end = wordEnd;
    +
    +        while (end < it->len && t[end] == CHAR_SPACE) {
    +            end++;
    +        }
    +
    +        // A word running to the run's end goes on in the runs after: it
    +        // fits only whole
    +        int32_t wordW  = textWidthN(font, t + pos, wordEnd - pos);
    +        int32_t tailW  = (wordEnd == it->len) ? wordTailWidth(flow, item) : 0;
    +        int32_t pieceW = wordW + textWidthN(font, t + wordEnd, end - wordEnd);
    +        bool    empty  = arrlen(flow->segs) == lb->firstSeg;
    +
    +        if (lb->x + wordW + tailW <= width || (empty && lb->x + wordW <= width)) {
    +            // It fits, or it is wider than a whole line and this run's part
    +            // of it fits (the line breaks inside it, between runs)
    +            linePlace(flow, lb, item, pos, end - pos, pieceW, wordW);
    +            pos         = end;
    +            lb->textPos = it->offset + pos;
    +        } else if (!empty) {
    +            lineEnd(flow, lb, true);
    +        } else {
    +            // A word wider than a whole line: as much as fits
    +            int32_t fit  = fitChars(font, t + pos, wordEnd - pos, width);
    +            int32_t fitW = textWidthN(font, t + pos, fit);
    +
    +            linePlace(flow, lb, item, pos, fit, fitW, fitW);
    +            pos         += fit;
    +            lb->textPos  = it->offset + pos;
    +            lineEnd(flow, lb, true);
    +        }
    +    }
    +}
    +
    +
    +// The x of line's left edge in the flow: aligned within the width laid
    +// out at (the widest line's, laid out at the natural width)
    +static int32_t lineAlignX(const TextFlowT *flow, const FlowLineT *line) {
    +    int32_t span = (flow->layoutW == FLOW_WIDTH_NATURAL) ? flow->widest : flow->layoutW;
    +
    +    if (line->align == FLOW_ALIGN_CENTER) {
    +        return (span - line->width) / 2;
    +    }
    +
    +    return (line->align == FLOW_ALIGN_RIGHT) ? span - line->width : 0;
    +}
    +
    +
    +// End the line being built (a wrap drops the next line's leading spaces)
    +static void lineEnd(TextFlowT *flow, FlowLineBuildT *lb, bool wrapped) {
    +    FlowLineT line;
    +    int32_t   segs = (int32_t)arrlen(flow->segs) - lb->firstSeg;
    +
    +    // A line's trailing spaces do not draw
    +    if (segs > 0) {
    +        FlowSegT        *last = &flow->segs[arrlen(flow->segs) - 1];
    +        const FlowItemT *item = &flow->items[last->item];
    +
    +        while (last->len > 0 && item->text[last->start + last->len - 1] == CHAR_SPACE) {
    +            last->len--;
    +        }
    +    }
    +
    +    // An empty line is as tall as the last font placed
    +    if (segs == 0 && lb->font) {
    +        lb->ascent  = lb->font->ascent;
    +        lb->descent = lb->font->charHeight - lb->font->ascent;
    +    }
    +
    +    memset(&line, 0, sizeof(line));
    +    line.firstSeg  = lb->firstSeg;
    +    line.segCount  = segs;
    +    line.y         = lb->y;
    +    line.ascent    = lb->ascent;
    +    line.height    = lb->ascent + lb->descent;
    +    line.width     = lb->contentW;
    +    line.align     = lb->align;
    +    line.textStart = lb->lineStart;
    +    arrput(flow->lines, line);
    +
    +    flow->widest = DVX_MAX(flow->widest, line.width);
    +    lb->y       += line.height;
    +    lb->x        = 0;
    +    lb->contentW = 0;
    +    lb->ascent   = 0;
    +    lb->descent  = 0;
    +    lb->firstSeg  = (int32_t)arrlen(flow->segs);
    +    lb->lineStart = lb->textPos;
    +    lb->wrapped   = wrapped;
    +}
    +
    +
    +// The offset line of the laid-out flow ends at: the next line's start, the
    +// text's end after the last
    +static int32_t lineEndOffset(const TextFlowT *flow, int32_t line) {
    +    return (line + 1 < arrlen(flow->lines)) ? flow->lines[line + 1].textStart : flow->textLen;
    +}
    +
    +
    +// Put len characters of item from start on the line being built: width
    +// wide with its trailing spaces, contentW without
    +static void linePlace(TextFlowT *flow, FlowLineBuildT *lb, int32_t item, int32_t start, int32_t len, int32_t width, int32_t contentW) {
    +    const BitmapFontT *font = flow->items[item].font;
    +    FlowSegT          *last = (arrlen(flow->segs) > lb->firstSeg) ? &flow->segs[arrlen(flow->segs) - 1] : NULL;
    +
    +    // The same run carried on: one segment
    +    if (last && last->item == item && last->start + last->len == start) {
    +        last->len += len;
    +    } else {
    +        FlowSegT seg;
    +
    +        seg.item  = item;
    +        seg.start = start;
    +        seg.len   = len;
    +        seg.x     = lb->x;
    +        arrput(flow->segs, seg);
    +    }
    +
    +    lb->contentW = lb->x + contentW;
    +    lb->x       += width;
    +    lb->ascent   = DVX_MAX(lb->ascent, font->ascent);
    +    lb->descent  = DVX_MAX(lb->descent, font->charHeight - font->ascent);
    +    lb->font     = font;
    +    lb->wrapped  = false;
    +}
    +
    +
    +// The x in the flow of the caret before offset, on line: in a segment,
    +// its start plus the characters before it; past the last (the spaces a
    +// line's end does not draw), the line's end plus theirs
    +static int32_t offsetX(const TextFlowT *flow, const FlowLineT *line, int32_t offset) {
    +    int32_t x     = lineAlignX(flow, line);
    +    int32_t endX  = 0;
    +    int32_t endAt = line->textStart;
    +    int32_t item  = 0;
    +
    +    for (int32_t s = line->firstSeg; s < line->firstSeg + line->segCount; s++) {
    +        const FlowSegT  *seg   = &flow->segs[s];
    +        const FlowItemT *it    = &flow->items[seg->item];
    +        int32_t          start = it->offset + seg->start;
    +
    +        if (offset <= start + seg->len) {
    +            return x + seg->x + textWidthN(it->font, it->text + seg->start, DVX_MAX(offset - start, 0));
    +        }
    +
    +        endX  = seg->x + textWidthN(it->font, it->text + seg->start, seg->len);
    +        endAt = start + seg->len;
    +    }
    +
    +    for (int32_t o = endAt; o < offset; o++) {
    +        endX += charWidth(flow, o, &item);
    +    }
    +
    +    return x + endX;
    +}
    +
    +
    +// Offsets from to to of segment seg (on line, its top at top) in fg
    +static void paintPart(const TextFlowT *flow, DisplayT *d, const BlitOpsT *ops, const FlowSegT *seg, int32_t from, int32_t to, int32_t x, int32_t top, const FlowLineT *line, uint32_t fg) {
    +    const FlowItemT *it    = &flow->items[seg->item];
    +    int32_t          skip  = from - it->offset - seg->start;
    +
    +    if (to <= from) {
    +        return;
    +    }
    +
    +    int32_t px = x + lineAlignX(flow, line) + seg->x + textWidthN(it->font, it->text + seg->start, skip);
    +
    +    drawTextN(d, ops, it->font, px, top + line->ascent - it->font->ascent, it->text + seg->start + skip, to - from, fg, 0, false);
    +}
    +
    +
    +// The width of the word that the end of text run item starts: its
    +// characters in the runs after, up to a space, a break, or a paragraph
    +static int32_t wordTailWidth(const TextFlowT *flow, int32_t item) {
    +    int32_t tailW = 0;
    +
    +    for (int32_t i = item + 1; i < arrlen(flow->items); i++) {
    +        const FlowItemT *it = &flow->items[i];
    +
    +        if (it->kind != FlowItemTextE) {
    +            break;
    +        }
    +
    +        const char *space = memchr(it->text, CHAR_SPACE, (size_t)it->len);
    +        int32_t     len   = space ? (int32_t)(space - it->text) : it->len;
    +
    +        tailW += textWidthN(it->font, it->text, len);
    +
    +        if (space) {
    +            break;
    +        }
    +    }
    +
    +    return tailW;
    +}
    diff --git a/src/libs/kpunch/libdvx/dvxFlow.h b/src/libs/kpunch/libdvx/dvxFlow.h
    new file mode 100644
    index 00000000..3ece625a
    --- /dev/null
    +++ b/src/libs/kpunch/libdvx/dvxFlow.h
    @@ -0,0 +1,123 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// dvxFlow.h  -- flowing text: runs of text in fonts and colors, laid out
    +// in wrapped, aligned lines
    +//
    +// A flow is a document built in order: runs of text (each in a font and a
    +// color), line breaks, and paragraph starts (each with an alignment).
    +// dvxFlowLayout wraps it to a width -- at spaces, or inside a word too wide
    +// for a whole line -- into lines as tall as the tallest font on them, all
    +// of a line's text standing on one baseline.  dvxFlowPaint draws the lines
    +// that meet the clip rectangle.  Underline and strike-through come with
    +// a run's font (dvxText.h).  Used by the RichLabel and RichEdit controls.
    +//
    +// Offsets: the text added is numbered from 0 in order, every run's
    +// characters after the one before (breaks and paragraphs take none).  An
    +// offset is the caret before that character.  Each line owns the offsets
    +// from its start to the next line's start (the spaces where it wrapped
    +// included), the last line those to the text's end.  Text that is all
    +// empty lays out as one empty line in its first run's font.
    +
    +#ifndef DVX_FLOW_H
    +#define DVX_FLOW_H
    +
    +#include "dvxTypes.h"
    +
    +#include 
    +#include 
    +
    +// Paragraph alignments
    +#define FLOW_ALIGN_LEFT    0
    +#define FLOW_ALIGN_CENTER  1
    +#define FLOW_ALIGN_RIGHT   2
    +
    +// A run's color: 0x00RRGGBB, or this for the color dvxFlowPaint is given
    +#define FLOW_COLOR_DEFAULT 0xFFFFFFFFu
    +
    +// dvxFlowLayout's width for a flow's natural size: no line wraps
    +#define FLOW_WIDTH_NATURAL INT32_MAX
    +
    +typedef struct TextFlowT TextFlowT;
    +
    +// An empty flow (one left-aligned paragraph); NULL out of memory.
    +TextFlowT *dvxFlowCreate(void);
    +
    +// Free a flow and give up the fonts it holds.  NULL is ignored.
    +void dvxFlowDestroy(TextFlowT *flow);
    +
    +// Empty a flow again.
    +void dvxFlowClear(TextFlowT *flow);
    +
    +// Add len bytes of text (len < 0: up to its NUL) in font, color
    +// (0x00RRGGBB or FLOW_COLOR_DEFAULT).  The flow holds font (dvxFontRetain)
    +// and copies the text.  False out of memory.
    +bool dvxFlowAddText(TextFlowT *flow, const char *text, int32_t len, BitmapFontT *font, uint32_t color);
    +
    +// Start a new line in the same paragraph.
    +bool dvxFlowAddBreak(TextFlowT *flow);
    +
    +// Start a new paragraph aligned FLOW_ALIGN_*; on an empty flow it sets
    +// the first paragraph's alignment.
    +bool dvxFlowAddParagraph(TextFlowT *flow, int32_t align);
    +
    +// Lay the flow out width pixels wide (FLOW_WIDTH_NATURAL: lines break only
    +// where breaks and paragraphs are).  Returns its height.  Laying out at
    +// the width it was last laid out at costs nothing (unless a run's font
    +// was built again since: dvxFontSetSmooth).
    +int32_t dvxFlowLayout(TextFlowT *flow, int32_t width);
    +
    +// The height and the widest line's width of the last layout.
    +int32_t dvxFlowHeight(const TextFlowT *flow);
    +int32_t dvxFlowWidth(const TextFlowT *flow);
    +
    +// Draw the laid-out flow with its top-left at x, y, aligned within the
    +// width it was laid out at; FLOW_COLOR_DEFAULT runs in defaultFg (a packed
    +// pixel).  Transparent: the background shows through.
    +void dvxFlowPaint(const TextFlowT *flow, DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, uint32_t defaultFg);
    +
    +// dvxFlowPaint with offsets selStart to selEnd selected: their text in
    +// selFg on selBg (packed pixels), a line's undrawn trailing spaces too.
    +void dvxFlowPaintSel(const TextFlowT *flow, DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, uint32_t defaultFg, int32_t selStart, int32_t selEnd, uint32_t selFg, uint32_t selBg);
    +
    +// The number of characters added (the last offset).
    +int32_t dvxFlowTextLength(const TextFlowT *flow);
    +
    +// Of the laid-out flow: a line's top and height; its lines; the line the
    +// caret before offset is on (a line's end is the next line's start; -1
    +// when there are no lines); the offset a line starts at (past the last
    +// line, the text's length).
    +void    dvxFlowLineBox(const TextFlowT *flow, int32_t line, int32_t *y, int32_t *h);
    +int32_t dvxFlowLineCount(const TextFlowT *flow);
    +int32_t dvxFlowLineOf(const TextFlowT *flow, int32_t offset);
    +int32_t dvxFlowLineStart(const TextFlowT *flow, int32_t line);
    +
    +// The caret before offset: its x, and its line's top and height, in the
    +// laid-out flow (from its top-left as painted).
    +void dvxFlowCaret(const TextFlowT *flow, int32_t offset, int32_t *x, int32_t *y, int32_t *h);
    +
    +// The offset nearest x, y (from the flow's top-left): on the line at y (the
    +// first or last beyond them), the character boundary nearest x.  A line
    +// that wrapped at a space ends before it.
    +int32_t dvxFlowHitTest(const TextFlowT *flow, int32_t x, int32_t y);
    +
    +#endif // DVX_FLOW_H
    diff --git a/src/libs/kpunch/libdvx/dvxFont.h b/src/libs/kpunch/libdvx/dvxFont.h
    index 8af78651..7b40d5c3 100644
    --- a/src/libs/kpunch/libdvx/dvxFont.h
    +++ b/src/libs/kpunch/libdvx/dvxFont.h
    @@ -55,6 +55,14 @@
     // size is FONT_CHAR_WIDTH x FONT_CHAR_HEIGHT (dvxTypes.h).
     #define FONT_NUM_CHARS 256
     
    +// Its rows: the baseline (letters sit on row 11), the accelerator mark
    +// and underline (the cell's last row) and the strike-through (the middle
    +// of the lower-case letters, rows 5-11)
    +#define FONT_SYS_ASCENT       12
    +#define FONT_SYS_UNDERLINE_Y  15
    +#define FONT_SYS_STRIKE_Y     8
    +#define FONT_SYS_LINE_THICK   1
    +
     // The glyph table and font descriptor have external linkage with a single
     // definition, compiled into the one translation unit that defines
     // DVX_FONT_IMPL before including this header (dvxWm.c).  Every other includer
    @@ -592,6 +600,17 @@ const BitmapFontT dvxFont8x16 = {
         .firstChar  = 0,
         .numChars   = FONT_NUM_CHARS,
         .glyphData  = font8x16,
    +    .glyphs     = NULL,
    +    .glyphAt    = NULL,
    +    .ascent     = FONT_SYS_ASCENT,
    +    .pixelSize  = FONT_CHAR_HEIGHT,
    +    .style      = 0,
    +    .flags      = 0,
    +    .underlineY = FONT_SYS_UNDERLINE_Y,
    +    .strikeY    = FONT_SYS_STRIKE_Y,
    +    .lineThick  = FONT_SYS_LINE_THICK,
    +    .priv       = NULL,
    +    .serial     = 0,
     };
     
     #endif // DVX_FONT_IMPL
    diff --git a/src/libs/kpunch/libdvx/dvxFontFmt.c b/src/libs/kpunch/libdvx/dvxFontFmt.c
    new file mode 100644
    index 00000000..a2de4525
    --- /dev/null
    +++ b/src/libs/kpunch/libdvx/dvxFontFmt.c
    @@ -0,0 +1,302 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// dvxFontFmt.c  -- reader of DVX font files (.DFN, dvxFontFmt.h)
    +//
    +// Shared by DVX, the font converter's preview, and the tests.  The font
    +// is never trusted: every count, offset, and size is checked against its
    +// length before it is used.
    +
    +#include "dvxFontFmt.h"
    +
    +#include 
    +#include 
    +#include 
    +
    +// A source being read: its bytes (a font in memory), else its file and
    +// where the font starts in it; the font's length
    +typedef struct {
    +    const uint8_t *data;
    +    FILE          *fp;
    +    uint32_t       base;
    +    uint32_t       length;
    +} DfnReaderT;
    +
    +// Code page 437 as Unicode (0: no character)
    +const uint16_t dfnCp437[DFN_CHAR_COUNT] = {
    +    0x0000, 0x263A, 0x263B, 0x2665, 0x2666, 0x2663, 0x2660, 0x2022, 0x25D8, 0x25CB, 0x25D9, 0x2642, 0x2640, 0x266A, 0x266B, 0x263C,
    +    0x25BA, 0x25C4, 0x2195, 0x203C, 0x00B6, 0x00A7, 0x25AC, 0x21A8, 0x2191, 0x2193, 0x2192, 0x2190, 0x221F, 0x2194, 0x25B2, 0x25BC,
    +    0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
    +    0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
    +    0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
    +    0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F,
    +    0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
    +    0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x2302,
    +    0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5,
    +    0x00C9, 0x00E6, 0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, 0x00FF, 0x00D6, 0x00DC, 0x00A2, 0x00A3, 0x00A5, 0x20A7, 0x0192,
    +    0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB,
    +    0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510,
    +    0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567,
    +    0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580,
    +    0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229,
    +    0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0
    +};
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +void        dfnFreeStrike(DfnStrikeDataT *s);
    +int32_t     dfnFromUnicode(uint32_t code);
    +bool        dfnLoadStrike(const DfnSourceT *src, const DfnFileT *f, int32_t index, DfnStrikeDataT *out, char *err, int32_t errSize);
    +char       *dfnLoadText(const DfnSourceT *src, const DfnFileT *f, DfnTextE which, char *err, int32_t errSize);
    +bool        dfnOpen(const DfnSourceT *src, DfnFileT *out, char *err, int32_t errSize);
    +void        dfnSourceFile(DfnSourceT *src, const char *path);
    +static bool fail(char *err, int32_t errSize, const DfnSourceT *src, const char *why);
    +static bool inFont(uint32_t length, uint32_t offset, uint32_t bytes);
    +static bool readAt(const DfnReaderT *r, uint32_t offset, void *buf, uint32_t len);
    +static void readerClose(DfnReaderT *r);
    +static bool readerOpen(const DfnSourceT *src, DfnReaderT *r);
    +
    +
    +void dfnFreeStrike(DfnStrikeDataT *s) {
    +    free(s->bitmaps);
    +    s->bitmaps = NULL;
    +}
    +
    +
    +// The code page 437 character that is Unicode code (the first, for one
    +// shown twice); -1 for none
    +int32_t dfnFromUnicode(uint32_t code) {
    +    for (int32_t c = 1; c < DFN_CHAR_COUNT; c++) {
    +        if (dfnCp437[c] == code) {
    +            return c;
    +        }
    +    }
    +
    +    return -1;
    +}
    +
    +
    +bool dfnLoadStrike(const DfnSourceT *src, const DfnFileT *f, int32_t index, DfnStrikeDataT *out, char *err, int32_t errSize) {
    +    DfnReaderT r;
    +
    +    memset(out, 0, sizeof(*out));
    +
    +    if (index < 0 || index >= f->header.strikeCount) {
    +        return fail(err, errSize, src, "has no such size");
    +    }
    +
    +    out->info = f->strikes[index];
    +
    +    if (!readerOpen(src, &r)) {
    +        return fail(err, errSize, src, "cannot be opened");
    +    }
    +
    +    uint32_t bytes = out->info.bitmapBytes;
    +    bool     ok    = readAt(&r, out->info.glyphOffset, out->glyphs, (uint32_t)sizeof(out->glyphs));
    +
    +    if (ok && bytes > 0) {
    +        out->bitmaps = (uint8_t *)malloc(bytes);
    +        ok           = out->bitmaps && readAt(&r, out->info.bitmapOffset, out->bitmaps, bytes);
    +    }
    +
    +    readerClose(&r);
    +
    +    if (!ok) {
    +        dfnFreeStrike(out);
    +        return fail(err, errSize, src, "cannot be read");
    +    }
    +
    +    // The bitmaps, one after another, fill the strike's block exactly
    +    uint32_t at  = 0;
    +    int32_t  bpp = (out->info.flags & DFN_STRIKE_GRAY2) ? FONT_GRAY_BPP : 1;
    +
    +    for (int32_t c = 0; c < DFN_CHAR_COUNT; c++) {
    +        out->bitmapAt[c]  = at;
    +        at               += FONT_GLYPH_BYTES_BPP(&out->glyphs[c], bpp);
    +    }
    +
    +    if (at != bytes) {
    +        dfnFreeStrike(out);
    +        return fail(err, errSize, src, "has glyphs that do not match its bitmaps");
    +    }
    +
    +    return true;
    +}
    +
    +
    +char *dfnLoadText(const DfnSourceT *src, const DfnFileT *f, DfnTextE which, char *err, int32_t errSize) {
    +    uint32_t offset = (which == DfnTextNoticeE) ? f->header.noticeOffset : f->header.licenceOffset;
    +    uint32_t bytes  = (which == DfnTextNoticeE) ? f->header.noticeBytes : f->header.licenceBytes;
    +    char    *text   = (char *)malloc((size_t)bytes + 1);
    +
    +    if (!text) {
    +        fail(err, errSize, src, "has a text too big for the memory left");
    +        return NULL;
    +    }
    +
    +    if (bytes > 0) {
    +        DfnReaderT r;
    +        bool       ok = readerOpen(src, &r) && readAt(&r, offset, text, bytes);
    +
    +        readerClose(&r);
    +
    +        if (!ok) {
    +            free(text);
    +            fail(err, errSize, src, "cannot be read");
    +            return NULL;
    +        }
    +    }
    +
    +    text[bytes] = '\0';
    +    return text;
    +}
    +
    +
    +bool dfnOpen(const DfnSourceT *src, DfnFileT *out, char *err, int32_t errSize) {
    +    DfnReaderT r;
    +
    +    memset(out, 0, sizeof(*out));
    +
    +    if (!readerOpen(src, &r)) {
    +        return fail(err, errSize, src, "cannot be opened");
    +    }
    +
    +    if (!readAt(&r, 0, &out->header, (uint32_t)sizeof(out->header))) {
    +        readerClose(&r);
    +        return fail(err, errSize, src, "cannot be read");
    +    }
    +
    +    const DfnHeaderT *h      = &out->header;
    +    int32_t           count  = h->strikeCount;
    +    uint32_t          length = r.length;
    +
    +    if (memcmp(h->magic, DFN_MAGIC, DFN_MAGIC_LEN) != 0 || h->version != DFN_VERSION) {
    +        readerClose(&r);
    +        return fail(err, errSize, src, "is not a DVX font file of this version");
    +    }
    +
    +    bool ok = count >= 1 && count <= DFN_STRIKE_MAX && readAt(&r, (uint32_t)sizeof(*h), out->strikes, (uint32_t)count * (uint32_t)sizeof(DfnStrikeT));
    +
    +    readerClose(&r);
    +
    +    if (!ok || memchr(h->family, '\0', sizeof(h->family)) == NULL || !inFont(length, h->noticeOffset, h->noticeBytes) || !inFont(length, h->licenceOffset, h->licenceBytes)) {
    +        return fail(err, errSize, src, "is damaged");
    +    }
    +
    +    out->fileSize = length;
    +
    +    // Sizes rising, a gray strike right after its size's 1-bit one (no
    +    // flags but that), each strike's tables inside the font
    +    for (int32_t i = 0; i < count; i++) {
    +        const DfnStrikeT *s      = &out->strikes[i];
    +        const DfnStrikeT *prev   = (i > 0) ? &out->strikes[i - 1] : NULL;
    +        uint32_t          glyphs = (uint32_t)sizeof(FontGlyphT) * DFN_CHAR_COUNT;
    +        bool              gray   = (s->flags & DFN_STRIKE_GRAY2) != 0;
    +        bool              order  = gray ? (prev && prev->pixelSize == s->pixelSize && !(prev->flags & DFN_STRIKE_GRAY2)) : (!prev || s->pixelSize > prev->pixelSize);
    +
    +        if (!order || (s->flags & ~(uint32_t)DFN_STRIKE_GRAY2) || !inFont(length, s->glyphOffset, glyphs) || !inFont(length, s->bitmapOffset, s->bitmapBytes)) {
    +            return fail(err, errSize, src, "is damaged");
    +        }
    +    }
    +
    +    return true;
    +}
    +
    +
    +void dfnSourceFile(DfnSourceT *src, const char *path) {
    +    memset(src, 0, sizeof(*src));
    +    src->name = path;
    +}
    +
    +
    +// The reason, with the font named.  Always false.
    +static bool fail(char *err, int32_t errSize, const DfnSourceT *src, const char *why) {
    +    if (err && errSize > 0) {
    +        snprintf(err, (size_t)errSize, "%s %s.", src->name ? src->name : "A font", why);
    +    }
    +
    +    return false;
    +}
    +
    +
    +// Whether bytes at offset lie inside a font length bytes long
    +static bool inFont(uint32_t length, uint32_t offset, uint32_t bytes) {
    +    return offset <= length && bytes <= length - offset;
    +}
    +
    +
    +// len bytes at offset in the font being read; false past its end
    +static bool readAt(const DfnReaderT *r, uint32_t offset, void *buf, uint32_t len) {
    +    if (!inFont(r->length, offset, len)) {
    +        return false;
    +    }
    +
    +    if (r->data) {
    +        memcpy(buf, r->data + offset, len);
    +        return true;
    +    }
    +
    +    return fseek(r->fp, (long)r->base + (long)offset, SEEK_SET) == 0 && fread(buf, 1, len, r->fp) == len;
    +}
    +
    +
    +static void readerClose(DfnReaderT *r) {
    +    if (r->fp) {
    +        fclose(r->fp);
    +        r->fp = NULL;
    +    }
    +}
    +
    +
    +// Open src for reading: its file, and the font's length (the rest of the
    +// file when src gives none); false when the file cannot be opened or the
    +// font does not fit in it
    +static bool readerOpen(const DfnSourceT *src, DfnReaderT *r) {
    +    memset(r, 0, sizeof(*r));
    +    r->data = src->data;
    +    r->base = src->base;
    +
    +    if (src->data) {
    +        r->length = src->length;
    +        return true;
    +    }
    +
    +    r->fp = src->name ? fopen(src->name, "rb") : NULL;
    +
    +    long end = (r->fp && fseek(r->fp, 0, SEEK_END) == 0) ? ftell(r->fp) : -1;
    +
    +    if (end < 0 || src->base > (uint32_t)end) {
    +        readerClose(r);
    +        return false;
    +    }
    +
    +    r->length = src->length ? src->length : (uint32_t)end - src->base;
    +
    +    if (r->length > (uint32_t)end - src->base) {
    +        readerClose(r);
    +        return false;
    +    }
    +
    +    return true;
    +}
    diff --git a/src/libs/kpunch/libdvx/dvxFontFmt.h b/src/libs/kpunch/libdvx/dvxFontFmt.h
    new file mode 100644
    index 00000000..0fec02a8
    --- /dev/null
    +++ b/src/libs/kpunch/libdvx/dvxFontFmt.h
    @@ -0,0 +1,176 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// dvxFontFmt.h  -- the DVX font file (.DFN)
    +//
    +// One file holds one family in one style (regular, bold, italic, or
    +// bold italic) at every pixel size it comes in ("strikes"), pre-rendered on the
    +// build host from TrueType, Type 1, or bitmap sources by mkfont
    +// (src/tools/mkfont.c), so DVX never rasterizes an outline.  Each strike
    +// holds the 256 characters of code page 437, 1 bit per pixel; a size can
    +// also come smoothed, FONT_GRAY_BPP bits per pixel (DFN_STRIKE_GRAY2).
    +//
    +// File layout (little-endian, as DOS and the build host both are; the
    +// records are written and read whole, every field naturally aligned so
    +// there is no padding):
    +//   DfnHeaderT                            at offset 0
    +//   DfnStrikeT [strikeCount]              right after it, smallest size first
    +//                                         (a size's 1-bit strike before its gray one)
    +//   per strike: FontGlyphT [DFN_CHAR_COUNT] at its glyphOffset
    +//               glyph bitmaps              at its bitmapOffset
    +//   the notice text and the licence text  at their offsets (after the
    +//                                         last strike)
    +//
    +// A glyph bitmap is height rows of (width + 7) / 8 bytes, the leftmost
    +// pixel in the top bit (a gray strike's: FONT_GRAY_BPP bits per pixel,
    +// dvxTypes.h).  The bitmaps follow one another in character
    +// order with nothing between them, so a glyph's place is the sum of the
    +// sizes before it (the reader works them out; the glyph records stay
    +// small).  A glyph the face does not have is marked FONT_GLYPH_MISSING
    +// (no bitmap): the converter never adds or changes a glyph, so a .DFN is
    +// a pure format conversion of its source.  The glyph record, the style
    +// bits, and the missing flag are the loaded font's own (dvxTypes.h).
    +//
    +// The file carries what its licence asks to go with every copy: the
    +// notice text (where the font came from and its copyright notices) and
    +// the licence text, ASCII with CR LF line ends, no NUL.  They are read
    +// only when asked for (dfnLoadText), never when the font is drawn.
    +//
    +// A .DFN is read from a source (DfnSourceT): a file of its own, a font
    +// inside another file (an application's font resource), or a font in
    +// memory.
    +
    +#ifndef DVX_FONT_FMT_H
    +#define DVX_FONT_FMT_H
    +
    +#include "dvxTypes.h"
    +
    +#include 
    +#include 
    +
    +#define DFN_MAGIC          "DVXF"
    +#define DFN_MAGIC_LEN      4
    +#define DFN_VERSION        2
    +#define DFN_EXT            ".DFN"
    +#define DFN_CHAR_COUNT     256
    +#define DFN_FAMILY_MAX     FONT_FAMILY_MAX
    +#define DFN_STRIKE_MAX     32   // sizes one file may hold
    +
    +// DfnStrikeT.flags
    +#define DFN_STRIKE_GRAY2   0x0001   // glyph bitmaps of FONT_GRAY_BPP bits per pixel (anti-aliased)
    +
    +// DfnHeaderT.faceFlags
    +#define DFN_FACE_FIXED     0x0001   // every glyph has the same advance
    +#define DFN_FACE_BOXDRAW   0x0002   // draw box / block / shade characters in place of the face's own
    +
    +typedef struct {
    +    char     magic[DFN_MAGIC_LEN];      // DFN_MAGIC, no NUL
    +    uint16_t version;                   // DFN_VERSION
    +    uint16_t style;                     // FONT_STYLE_BOLD / FONT_STYLE_ITALIC
    +    uint16_t faceFlags;                 // DFN_FACE_*
    +    uint16_t strikeCount;               // 1 .. DFN_STRIKE_MAX
    +    char     family[DFN_FAMILY_MAX];    // "DejaVu Sans", NUL-padded
    +    uint32_t noticeOffset;              // the notice text: where it starts in the file
    +    uint32_t noticeBytes;               // and its length
    +    uint32_t licenceOffset;             // the licence text
    +    uint32_t licenceBytes;
    +} DfnHeaderT;
    +
    +typedef struct {
    +    uint32_t glyphOffset;   // file offset of the strike's DFN_CHAR_COUNT glyph records
    +    uint32_t bitmapOffset;  // file offset of its glyph bitmaps
    +    uint32_t bitmapBytes;   // their size
    +    uint16_t pixelSize;     // the size it was made at (a bitmap face's own size)
    +    int16_t  ascent;        // rows above the baseline (the face's or its tallest glyph's)
    +    int16_t  descent;       // rows below the baseline
    +    int16_t  lineHeight;    // baseline to baseline
    +    uint16_t maxAdvance;    // widest advance
    +    uint16_t flags;         // DFN_STRIKE_*
    +} DfnStrikeT;
    +
    +_Static_assert(sizeof(DfnHeaderT) == 60, "DfnHeaderT is written whole: no padding");
    +_Static_assert(sizeof(DfnStrikeT) == 24, "DfnStrikeT is written whole: no padding");
    +
    +// ============================================================
    +// Reader (dvxFontFmt.c): the header and strike table when a font is
    +// opened, one strike's glyphs and bitmaps or a text when it is needed.
    +// Every offset and size is checked against the font's length, so a
    +// damaged font is refused, never read past.
    +// ============================================================
    +
    +#define DFN_ERR_MAX        128  // reason text from the reader
    +
    +// Where a .DFN is: in memory (data), else in the file name at base,
    +// length bytes long (0: to the end of the file).  name also names the
    +// font in the reader's messages.
    +typedef struct {
    +    const char    *name;
    +    const uint8_t *data;
    +    uint32_t       base;
    +    uint32_t       length;
    +} DfnSourceT;
    +
    +// The texts a .DFN carries (dfnLoadText)
    +typedef enum {
    +    DfnTextNoticeE,
    +    DfnTextLicenceE
    +} DfnTextE;
    +
    +typedef struct {
    +    DfnHeaderT header;
    +    DfnStrikeT strikes[DFN_STRIKE_MAX];
    +    uint32_t   fileSize;        // the font's length
    +} DfnFileT;
    +
    +typedef struct {
    +    DfnStrikeT info;
    +    FontGlyphT glyphs[DFN_CHAR_COUNT];
    +    uint32_t   bitmapAt[DFN_CHAR_COUNT];    // each glyph's offset in bitmaps
    +    uint8_t   *bitmaps;                     // info.bitmapBytes bytes (malloc'd; NULL when none)
    +} DfnStrikeDataT;
    +
    +// Code page 437 (the character set of .DFN files and of DVX text) as
    +// Unicode: dfnCp437[c] is character c's code point (0: none);
    +// dfnFromUnicode is the character for a code point, -1 for none.
    +extern const uint16_t dfnCp437[DFN_CHAR_COUNT];
    +int32_t dfnFromUnicode(uint32_t code);
    +
    +// Free a strike dfnLoadStrike filled (safe on a zeroed one).
    +void dfnFreeStrike(DfnStrikeDataT *s);
    +
    +// Load strike index of the font at src (dfnOpen gave f), working out
    +// where each glyph's bitmap starts.  False with a reason in err when it
    +// cannot be read or its bitmaps do not fill its block exactly.
    +bool dfnLoadStrike(const DfnSourceT *src, const DfnFileT *f, int32_t index, DfnStrikeDataT *out, char *err, int32_t errSize);
    +
    +// Text which of the font at src (dfnOpen gave f), NUL-terminated
    +// (malloc'd: the caller frees it; "" when the font has none).  NULL with
    +// a reason in err when it cannot be read or memory runs out.
    +char *dfnLoadText(const DfnSourceT *src, const DfnFileT *f, DfnTextE which, char *err, int32_t errSize);
    +
    +// Read and check the header and strike table of the font at src.
    +bool dfnOpen(const DfnSourceT *src, DfnFileT *out, char *err, int32_t errSize);
    +
    +// src as the whole file at path.
    +void dfnSourceFile(DfnSourceT *src, const char *path);
    +
    +#endif // DVX_FONT_FMT_H
    diff --git a/src/libs/kpunch/libdvx/dvxRes.h b/src/libs/kpunch/libdvx/dvxRes.h
    index 43df9800..8324e003 100644
    --- a/src/libs/kpunch/libdvx/dvxRes.h
    +++ b/src/libs/kpunch/libdvx/dvxRes.h
    @@ -53,6 +53,7 @@
     #define DVX_RES_ICON     1   // image data (BMP icon: 16x16, 32x32, etc.)
     #define DVX_RES_TEXT     2   // null-terminated string (author, copyright, etc.)
     #define DVX_RES_BINARY   3   // arbitrary binary data (app-specific)
    +#define DVX_RES_FONT     4   // a .DFN font the program uses (dvxText.h: added for it when it loads)
     
     // Footer magic: "DVXR" as a 32-bit little-endian value
     #define DVX_RES_MAGIC    0x52585644UL
    @@ -68,7 +69,7 @@
     // Directory entry (48 bytes each)
     typedef struct {
         char     name[DVX_RES_NAME_MAX];  // resource name (e.g. "icon16", "author")
    -    uint32_t type;                    // DVX_RES_ICON, DVX_RES_TEXT, DVX_RES_BINARY
    +    uint32_t type;                    // DVX_RES_ICON, DVX_RES_TEXT, DVX_RES_BINARY, DVX_RES_FONT
         uint32_t offset;                  // absolute file offset of data
         uint32_t size;                    // data size in bytes
         uint32_t reserved;                // padding for future use
    diff --git a/src/libs/kpunch/libdvx/dvxText.c b/src/libs/kpunch/libdvx/dvxText.c
    new file mode 100644
    index 00000000..92f5c1b8
    --- /dev/null
    +++ b/src/libs/kpunch/libdvx/dvxText.c
    @@ -0,0 +1,1644 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// dvxText.c  -- fonts: the family list, loading, filling in and caching
    +// (dvxText.h)
    +//
    +// A loaded font is built once, character by character, into its own
    +// glyph table and bitmaps, so drawing never decides anything per glyph:
    +//   - a box drawing, block or shade character (CP437 B0-DF) the face
    +//     lacks, or every one when its file asks (DFN_FACE_BOXDRAW), is drawn
    +//     here to fill the character cell, so lines join across characters;
    +//   - a character the face has is copied, thickened and / or slanted when
    +//     the style is synthesized (box characters never are: they must
    +//     still join);
    +//   - any other character the face lacks comes from the fallback family
    +//     at the nearest size in the same style (centered in the pitch of a
    +//     fixed-pitch face), or from the system font when that family is
    +//     not there;
    +//   - what nothing has is a blank as wide as a space.
    +// Glyphs are made one byte per pixel (its level: 0 none .. FONT_GRAY_FULL
    +// full) whatever they come from, and stored at the font's depth: 1 bit
    +// (a level of FONT_GRAY_HALF or more is set) or, for a smoothed font,
    +// FONT_GRAY_BPP bits.  A smoothed font is built from a size's gray strike
    +// when smoothing is on (dvxFontSetSmooth) and the face has one.
    +// Everything here is charged to the system (app 0), not to the program
    +// that happened to open a font: the cache outlives it.
    +//
    +// Families are installed (the FONTS folder, everyone's) or added for one
    +// program (owner: its app id) from a file, a font resource, or memory.
    +// A program sees the installed families and its own; where it has a
    +// family that is installed too, its faces and sizes come first and the
    +// installed family fills in the styles and sizes it lacks.  Fonts built
    +// for a program with families of its own are cached for it alone (their
    +// fallback glyphs may come from its families too); every other program
    +// shares the cache.
    +
    +#include "dvxText.h"
    +#include "dvxFont.h"
    +#include "dvxFontFmt.h"
    +#include "dvxMem.h"
    +#include "dvxPlat.h"
    +#include "dvxRes.h"
    +#include "stb_ds_wrap.h"
    +
    +#include 
    +#include 
    +#include 
    +#include 
    +#include 
    +
    +#define FACE_STYLES         4           // regular, bold, italic, bold italic: indexed by style bits
    +#define FONT_IDLE_MAX       8           // fonts no one holds kept loaded
    +#define SYSTEM_FAMILY_INDEX 0
    +#define NO_FAMILY           (-1)
    +#define OWNER_ALL           0           // an installed family's owner: every program's
    +#define OWNER_GONE          (-1)        // a font built for fonts since removed: never found again
    +#define TEXT_EOL            "\r\n"      // between a font's notice and licence texts
    +#define CHAR_SPACE          0x20
    +#define CHAR_ZERO           0x30
    +#define CHAR_X              0x78
    +
    +// CP437 shades, box drawing and blocks
    +#define BOX_FIRST           0xB0
    +#define BOX_LAST            0xDF
    +#define SHADE_LIGHT         0xB0
    +#define SHADE_MEDIUM        0xB1
    +#define SHADE_DARK          0xB2
    +#define BOX_LINES_FIRST     0xB3        // first of the line characters (sBoxArms)
    +#define BOX_LINES_LAST      0xDA
    +#define BLOCK_FULL          0xDB
    +#define BLOCK_LOWER         0xDC
    +#define BLOCK_LEFT          0xDD
    +#define BLOCK_RIGHT         0xDE
    +#define BLOCK_UPPER         0xDF
    +#define SHADE_LIGHT_STEP    4           // light shade: 1 pixel in 4 per row
    +#define SHADE_LIGHT_SHIFT   2           // ... offset this much on odd rows
    +
    +// A line character's arms: 2 bits each (0 none, 1 single, 2 double)
    +#define ARM_NONE            0
    +#define ARM_SINGLE          1
    +#define ARM_DOUBLE          2
    +#define ARM_BITS            2
    +#define ARM_MASK            3
    +#define ARMS(u, d, l, r)    (uint8_t)(((u) << (ARM_BITS * 3)) | ((d) << (ARM_BITS * 2)) | ((l) << ARM_BITS) | (r))
    +#define ARM_UP(a)           (((a) >> (ARM_BITS * 3)) & ARM_MASK)
    +#define ARM_DOWN(a)         (((a) >> (ARM_BITS * 2)) & ARM_MASK)
    +#define ARM_LEFT(a)         (((a) >> ARM_BITS) & ARM_MASK)
    +#define ARM_RIGHT(a)        ((a) & ARM_MASK)
    +
    +// Synthesized styles and lines
    +#define SYNTH_BOLD_STEP     24          // a made bold thickens 1 column, plus 1 per this many pixels of size
    +#define SYNTH_SLANT         4           // a made italic leans 1 column per this many rows
    +#define LINE_STEP           24          // underline / strike-through: 1 row, plus 1 per this many pixels of size
    +#define UNDERLINE_DROP_DIV  3           // the underline sits this fraction of the descent below the baseline
    +
    +// Where a face's .DFN is: a file (path, the font at base, length bytes;
    +// 0: to the file's end) or a copy in memory (data).  Counted: held by
    +// its face and by every font built from it, so a font always knows its
    +// own licence, its face gone or not.
    +typedef struct {
    +    int32_t  refs;
    +    char    *path;                      // the file, or a name for a font in memory (malloc'd)
    +    uint8_t *data;                      // a font in memory (malloc'd copy); NULL for a file
    +    uint32_t base;
    +    uint32_t length;
    +} FontSourceT;
    +
    +// One .DFN: a face of a family, installed (in DVX_FONTS_DIR) or added
    +typedef struct {
    +    FontSourceT *src;                   // held; NULL: no face of this style (sizeCount 0)
    +    bool         installed;             // read from DVX_FONTS_DIR (a rescan reads it again)
    +    uint8_t      sizes[DFN_STRIKE_MAX]; // rising, each once
    +    int32_t      sizeCount;
    +    uint32_t     grayMask;              // bit i: sizes[i] has a gray strike too
    +    uint16_t     style;                 // FONT_STYLE_BOLD / ITALIC
    +    uint16_t     faceFlags;             // DFN_FACE_*
    +} FontFaceT;
    +
    +// A family: its faces by style, installed (OWNER_ALL) or added for one
    +// program
    +typedef struct {
    +    char      name[DFN_FAMILY_MAX];
    +    int32_t   owner;
    +    FontFaceT faces[FACE_STYLES];
    +} FontFamilyT;
    +
    +// What a font is built from: the face (NULL: the system font) at size,
    +// the style bits made here, and the family's name
    +typedef struct {
    +    const FontFaceT *face;
    +    int32_t          size;
    +    uint32_t         synth;
    +    bool             gray;              // from the size's gray strike
    +    char             family[DFN_FAMILY_MAX];
    +} FontPickT;
    +
    +// A loaded font: the BitmapFontT handed out is its first member.  A font
    +// with lines (underline, strike-through) is the font without them (base,
    +// held) with the line bits in its style: the same glyphs.
    +typedef struct FontEntryT {
    +    BitmapFontT        font;
    +    FontGlyphT         glyphs[DFN_CHAR_COUNT];
    +    uint32_t           glyphAt[DFN_CHAR_COUNT];
    +    uint8_t           *bitmaps;         // stb_ds; NULL for the plain system font and a font with lines
    +    char               family[DFN_FAMILY_MAX];
    +    int32_t            owner;           // whose families it was built from (OWNER_ALL: everyone's)
    +    FontSourceT       *src;             // the face it was built from (held); NULL: the system font, a font with lines
    +    struct FontEntryT *base;            // a font with lines: the font it draws
    +    int32_t            refs;
    +    uint32_t           lastUse;         // sUseClock when last given up
    +    struct FontEntryT *next;
    +} FontEntryT;
    +
    +// What a font is built from (entryBuild)
    +typedef struct {
    +    const DfnStrikeDataT *strike;       // the face's; NULL: the system font
    +    BitmapFontT          *fallback;     // fills what the face lacks (opened when first needed)
    +    const char           *fallbackName;
    +    uint32_t              style;
    +    uint32_t              synth;        // FONT_STYLE_* made here
    +    bool                  boxDraw;      // draw every box character here
    +    int32_t               boldExtra;    // columns a made bold adds
    +    int32_t               pitch;        // the built font's pitch; 0: proportional
    +    int32_t               spaceW;       // the built font's space width
    +    int32_t               cellW;        // box character cell width
    +    int32_t               lineThick;    // underline, strike-through and box line rows
    +    int32_t               ascent;
    +    int32_t               lineHeight;
    +    int32_t               pixelSize;
    +    int32_t               bpp;          // the built font's bits per pixel
    +    uint8_t              *px;           // stb_ds: the glyph being made, one level per pixel
    +    uint8_t              *spare;        // stb_ds: where the next step puts it
    +} BuildT;
    +
    +// The arms of CP437 B3-DA (up, down, left, right)
    +static const uint8_t sBoxArms[BOX_LINES_LAST - BOX_LINES_FIRST + 1] = {
    +    ARMS(1, 1, 0, 0), ARMS(1, 1, 1, 0), ARMS(1, 1, 2, 0), ARMS(2, 2, 1, 0),     // B3 B4 B5 B6
    +    ARMS(0, 2, 1, 0), ARMS(0, 1, 2, 0), ARMS(2, 2, 2, 0), ARMS(2, 2, 0, 0),     // B7 B8 B9 BA
    +    ARMS(0, 2, 2, 0), ARMS(2, 0, 2, 0), ARMS(2, 0, 1, 0), ARMS(1, 0, 2, 0),     // BB BC BD BE
    +    ARMS(0, 1, 1, 0), ARMS(1, 0, 0, 1), ARMS(1, 0, 1, 1), ARMS(0, 1, 1, 1),     // BF C0 C1 C2
    +    ARMS(1, 1, 0, 1), ARMS(0, 0, 1, 1), ARMS(1, 1, 1, 1), ARMS(1, 1, 0, 2),     // C3 C4 C5 C6
    +    ARMS(2, 2, 0, 1), ARMS(2, 0, 0, 2), ARMS(0, 2, 0, 2), ARMS(2, 0, 2, 2),     // C7 C8 C9 CA
    +    ARMS(0, 2, 2, 2), ARMS(2, 2, 0, 2), ARMS(0, 0, 2, 2), ARMS(2, 2, 2, 2),     // CB CC CD CE
    +    ARMS(1, 0, 2, 2), ARMS(2, 0, 1, 1), ARMS(0, 1, 2, 2), ARMS(0, 2, 1, 1),     // CF D0 D1 D2
    +    ARMS(2, 0, 0, 1), ARMS(1, 0, 0, 2), ARMS(0, 1, 0, 2), ARMS(0, 2, 0, 1),     // D3 D4 D5 D6
    +    ARMS(2, 2, 1, 1), ARMS(1, 1, 2, 2), ARMS(1, 0, 1, 0), ARMS(0, 1, 0, 1)      // D7 D8 D9 DA
    +};
    +
    +// The faces a style is drawn from, best first: the exact one, then
    +// ones lacking a style bit (made here), then any
    +static const uint32_t sFaceOrder[FACE_STYLES][FACE_STYLES] = {
    +    { 0,                                   FONT_STYLE_BOLD,   FONT_STYLE_ITALIC,                   FONT_STYLE_BOLD | FONT_STYLE_ITALIC },
    +    { FONT_STYLE_BOLD,                     0,                 FONT_STYLE_BOLD | FONT_STYLE_ITALIC, FONT_STYLE_ITALIC                   },
    +    { FONT_STYLE_ITALIC,                   0,                 FONT_STYLE_BOLD | FONT_STYLE_ITALIC, FONT_STYLE_BOLD                     },
    +    { FONT_STYLE_BOLD | FONT_STYLE_ITALIC, FONT_STYLE_BOLD,   FONT_STYLE_ITALIC,                   0                                   }
    +};
    +
    +// The glyphData of a font whose glyphs are all empty: never NULL, so
    +// glyphData + glyphAt[c] is a pointer for every character
    +static const uint8_t sNoBitmaps[1];
    +
    +static FontFamilyT *sFamilies;          // stb_ds; [SYSTEM_FAMILY_INDEX] is the system font
    +static bool         sScanned;
    +static const char **sView;              // stb_ds: the family names sViewOwner sees ("System" first, then alphabetical)
    +static int32_t      sViewOwner = OWNER_GONE;  // whose sView is; OWNER_GONE: none made
    +static FontEntryT  *sEntries;
    +static uint32_t     sUseClock;
    +static uint32_t     sSerial;            // the last loaded font's serial
    +static bool         sSmooth;            // dvxFontSetSmooth
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +static void         boxGlyph(BuildT *b, int32_t ch, FontGlyphT *g);
    +static void         boxLines(uint8_t *px, int32_t w, int32_t h, int32_t t, uint8_t arms);
    +bool                dvxFontAddFile(int32_t owner, const char *path);
    +bool                dvxFontAddMemory(int32_t owner, const void *data, uint32_t length, const char *name);
    +int32_t             dvxFontAddResources(int32_t owner, const char *path);
    +void                dvxFontClose(BitmapFontT *font);
    +int32_t             dvxFontFamilyCount(void);
    +bool                dvxFontFamilyFixed(const char *family);
    +const char         *dvxFontFamilyName(int32_t index);
    +const char         *dvxFontFamilyOf(const BitmapFontT *font);
    +char               *dvxFontLicence(const BitmapFontT *font);
    +BitmapFontT        *dvxFontOpen(const char *family, int32_t pixelSize, uint32_t style);
    +void                dvxFontRemoveOwner(int32_t owner);
    +void                dvxFontRescan(void);
    +BitmapFontT        *dvxFontRetain(BitmapFontT *font);
    +void                dvxFontSetSmooth(bool smooth);
    +int32_t             dvxFontSizes(const char *family, uint32_t style, int32_t *sizes, int32_t max);
    +bool                dvxFontSmooth(void);
    +static FontEntryT  *entryBuild(int32_t owner, const FontPickT *pick, uint32_t style);
    +static void         entryFinish(FontEntryT *e, const BuildT *b);
    +static void         entryFree(FontEntryT *e);
    +static FontEntryT  *entryLines(int32_t owner, const FontPickT *pick, uint32_t style);
    +static void         entryLinesCopy(FontEntryT *e, uint32_t lines);
    +static void         entryPut(FontEntryT *e, const BuildT *b, int32_t ch, const FontGlyphT *g);
    +static void         entryRebuild(FontEntryT *e);
    +static bool         faceAdd(int32_t owner, const DfnSourceT *src);
    +static void         faceFill(FontFaceT *face, const DfnFileT *file);
    +static void         faceFree(FontFaceT *face);
    +static bool         faceGlyph(const BuildT *b, int32_t ch, FontGlyphT *g, const uint8_t **bits);
    +static bool         faceGray(const FontFaceT *face, int32_t size);
    +static void         familyDropEmpty(int32_t fam);
    +static int32_t      familyFind(const char *name, int32_t owner);
    +static void         familyFree(FontFamilyT *fam);
    +static bool         familyKnown(const char *name, int32_t owner);
    +static int32_t      familyMake(const char *name, int32_t owner);
    +static BitmapFontT *fontOpen(int32_t owner, const char *family, int32_t pixelSize, uint32_t style);
    +static void         fontPick(int32_t owner, const char *family, int32_t pixelSize, uint32_t style, FontPickT *pick);
    +static bool         glyphView(const BitmapFontT *font, int32_t ch, FontGlyphT *g, const uint8_t **bits);
    +static void         idleTrim(int32_t keep);
    +static bool         isBox(int32_t ch);
    +static void         levelRect(uint8_t *px, int32_t w, int32_t h, int32_t x0, int32_t y0, int32_t x1, int32_t y1);
    +static void         levelsFrom(BuildT *b, const FontGlyphT *g, const uint8_t *bits, int32_t bpp);
    +static int32_t      memAsSystem(void);
    +static void         memRestore(int32_t prev);
    +static int          nameCompare(const void *a, const void *b);
    +static int32_t      nearestSize(const FontFaceT *a, const FontFaceT *b, int32_t pixelSize);
    +static void         ownerDetach(int32_t owner, const char *family);
    +static bool         pickFaces(int32_t owner, const char *family, uint32_t style, const FontFaceT **mine, const FontFaceT **base, uint32_t *synth);
    +static void         scanFonts(void);
    +static int32_t      scopeOwner(void);
    +static int32_t      sizeIndex(const FontFaceT *face, int32_t size);
    +static void         sourceDfn(const FontSourceT *src, DfnSourceT *out);
    +static FontSourceT *sourceMake(const DfnSourceT *dfn);
    +static void         sourceRelease(FontSourceT *src);
    +static void         synthBold(BuildT *b, FontGlyphT *g);
    +static void         synthItalic(BuildT *b, FontGlyphT *g);
    +static int          viewCompare(const void *a, const void *b);
    +static void         viewFor(int32_t owner);
    +static uint8_t     *workArea(uint8_t **buf, int32_t bytes);
    +
    +
    +// Box drawing, block or shade character ch drawn as the glyph being made,
    +// filling a character cell (cellW x the line height) at the full level
    +static void boxGlyph(BuildT *b, int32_t ch, FontGlyphT *g) {
    +    int32_t  w  = b->cellW;
    +    int32_t  h  = b->lineHeight;
    +    uint8_t *px = workArea(&b->px, w * h);
    +
    +    if (ch >= BOX_LINES_FIRST && ch <= BOX_LINES_LAST) {
    +        boxLines(px, w, h, b->lineThick, sBoxArms[ch - BOX_LINES_FIRST]);
    +    } else if (ch == BLOCK_FULL) {
    +        levelRect(px, w, h, 0, 0, w, h);
    +    } else if (ch == BLOCK_LOWER) {
    +        levelRect(px, w, h, 0, h / 2, w, h);
    +    } else if (ch == BLOCK_LEFT) {
    +        levelRect(px, w, h, 0, 0, w / 2, h);
    +    } else if (ch == BLOCK_RIGHT) {
    +        levelRect(px, w, h, w / 2, 0, w, h);
    +    } else if (ch == BLOCK_UPPER) {
    +        levelRect(px, w, h, 0, 0, w, h / 2);
    +    } else {
    +        // Shades: a quarter, half or three quarters of the pixels
    +        for (int32_t y = 0; y < h; y++) {
    +            for (int32_t x = 0; x < w; x++) {
    +                bool light = (x + (y % 2) * SHADE_LIGHT_SHIFT) % SHADE_LIGHT_STEP == 0;
    +                bool on    = (ch == SHADE_LIGHT) ? light : (ch == SHADE_MEDIUM) ? (x + y) % 2 == 0 : !light;
    +
    +                px[y * w + x] = on ? FONT_GRAY_FULL : 0;
    +            }
    +        }
    +    }
    +
    +    g->advance = (uint8_t)w;
    +    g->width   = (uint8_t)w;
    +    g->height  = (uint8_t)h;
    +    g->left    = 0;
    +    g->top     = (int8_t)b->ascent;
    +    g->flags   = 0;
    +}
    +
    +
    +// The arms of a line character, t pixels thick, meeting at the cell's
    +// center.  A single line runs through the center; a double line is two
    +// lines t apart either side of it.  Arms stop where a double line across
    +// them begins, and a double arm's lines turn into the double line across
    +// it, so corners, tees and crossings join as box drawing does.
    +static void boxLines(uint8_t *px, int32_t w, int32_t h, int32_t t, uint8_t arms) {
    +    int32_t cx    = (w - t) / 2;
    +    int32_t cy    = (h - t) / 2;
    +    int32_t up    = ARM_UP(arms);
    +    int32_t down  = ARM_DOWN(arms);
    +    int32_t left  = ARM_LEFT(arms);
    +    int32_t right = ARM_RIGHT(arms);
    +    bool    vDbl  = up == ARM_DOUBLE || down == ARM_DOUBLE;
    +    bool    hDbl  = left == ARM_DOUBLE || right == ARM_DOUBLE;
    +
    +    if (left == ARM_SINGLE) {
    +        levelRect(px, w, h, 0, cy, vDbl ? cx : cx + t, cy + t);
    +    } else if (left == ARM_DOUBLE) {
    +        levelRect(px, w, h, 0, cy - t, up == ARM_DOUBLE ? cx : cx + 2 * t, cy);
    +        levelRect(px, w, h, 0, cy + t, down == ARM_DOUBLE ? cx : cx + 2 * t, cy + 2 * t);
    +    }
    +
    +    if (right == ARM_SINGLE) {
    +        levelRect(px, w, h, vDbl ? cx + t : cx, cy, w, cy + t);
    +    } else if (right == ARM_DOUBLE) {
    +        levelRect(px, w, h, up == ARM_DOUBLE ? cx + t : cx - t, cy - t, w, cy);
    +        levelRect(px, w, h, down == ARM_DOUBLE ? cx + t : cx - t, cy + t, w, cy + 2 * t);
    +    }
    +
    +    if (up == ARM_SINGLE) {
    +        levelRect(px, w, h, cx, 0, cx + t, hDbl ? cy : cy + t);
    +    } else if (up == ARM_DOUBLE) {
    +        levelRect(px, w, h, cx - t, 0, cx, left == ARM_DOUBLE ? cy : cy + 2 * t);
    +        levelRect(px, w, h, cx + t, 0, cx + 2 * t, right == ARM_DOUBLE ? cy : cy + 2 * t);
    +    }
    +
    +    if (down == ARM_SINGLE) {
    +        levelRect(px, w, h, cx, hDbl ? cy + t : cy, cx + t, h);
    +    } else if (down == ARM_DOUBLE) {
    +        levelRect(px, w, h, cx - t, left == ARM_DOUBLE ? cy + t : cy - t, cx, h);
    +        levelRect(px, w, h, cx + t, right == ARM_DOUBLE ? cy + t : cy - t, cx + 2 * t, h);
    +    }
    +}
    +
    +
    +bool dvxFontAddFile(int32_t owner, const char *path) {
    +    DfnSourceT src;
    +
    +    dfnSourceFile(&src, path);
    +    return faceAdd(owner, &src);
    +}
    +
    +
    +bool dvxFontAddMemory(int32_t owner, const void *data, uint32_t length, const char *name) {
    +    DfnSourceT src;
    +
    +    memset(&src, 0, sizeof(src));
    +    src.name   = name;
    +    src.data   = (const uint8_t *)data;
    +    src.length = length;
    +    return data && faceAdd(owner, &src);
    +}
    +
    +
    +int32_t dvxFontAddResources(int32_t owner, const char *path) {
    +    DvxResHandleT *res   = path ? dvxResOpen(path) : NULL;
    +    int32_t        added = 0;
    +
    +    for (uint32_t i = 0; res && i < res->entryCount; i++) {
    +        const DvxResDirEntryT *r = &res->entries[i];
    +        DfnSourceT             src;
    +
    +        if (r->type != DVX_RES_FONT) {
    +            continue;
    +        }
    +
    +        dfnSourceFile(&src, path);
    +        src.base   = r->offset;
    +        src.length = r->size;
    +        added     += faceAdd(owner, &src) ? 1 : 0;
    +    }
    +
    +    if (res) {
    +        dvxResClose(res);
    +    }
    +
    +    return added;
    +}
    +
    +
    +void dvxFontClose(BitmapFontT *font) {
    +    FontEntryT *e = font ? (FontEntryT *)font->priv : NULL;
    +
    +    if (!e || e->refs <= 0) {
    +        return;
    +    }
    +
    +    if (--e->refs == 0) {
    +        e->lastUse = ++sUseClock;
    +        idleTrim(FONT_IDLE_MAX);
    +    }
    +}
    +
    +
    +int32_t dvxFontFamilyCount(void) {
    +    viewFor(scopeOwner());
    +    return (int32_t)arrlen(sView);
    +}
    +
    +
    +bool dvxFontFamilyFixed(const char *family) {
    +    int32_t owner = scopeOwner();
    +    bool    fixed = true;
    +
    +    scanFonts();
    +
    +    int32_t inst = familyFind(family, OWNER_ALL);
    +    int32_t own  = (owner != OWNER_ALL) ? familyFind(family, owner) : NO_FAMILY;
    +
    +    if (inst == NO_FAMILY && own == NO_FAMILY) {
    +        return false;
    +    }
    +
    +    // Every face either family has (none: the system font, fixed)
    +    for (int32_t i = 0; i < FACE_STYLES; i++) {
    +        fixed = fixed && (inst == NO_FAMILY || !sFamilies[inst].faces[i].sizeCount || (sFamilies[inst].faces[i].faceFlags & DFN_FACE_FIXED));
    +        fixed = fixed && (own == NO_FAMILY || !sFamilies[own].faces[i].sizeCount || (sFamilies[own].faces[i].faceFlags & DFN_FACE_FIXED));
    +    }
    +
    +    return fixed;
    +}
    +
    +
    +const char *dvxFontFamilyName(int32_t index) {
    +    viewFor(scopeOwner());
    +    return (index >= 0 && index < arrlen(sView)) ? sView[index] : NULL;
    +}
    +
    +
    +const char *dvxFontFamilyOf(const BitmapFontT *font) {
    +    const FontEntryT *e = font ? (const FontEntryT *)font->priv : NULL;
    +
    +    return e ? e->family : FONT_SYSTEM_FAMILY;
    +}
    +
    +
    +// The notice and licence texts of the face font was built from, a blank
    +// line between them
    +char *dvxFontLicence(const BitmapFontT *font) {
    +    const FontEntryT *e = font ? (const FontEntryT *)font->priv : NULL;
    +    DfnSourceT        src;
    +    DfnFileT          file;
    +    char              err[DFN_ERR_MAX] = "out of memory";
    +
    +    while (e && e->base) {
    +        e = e->base;
    +    }
    +
    +    if (!e || !e->src) {
    +        return NULL;
    +    }
    +
    +    sourceDfn(e->src, &src);
    +
    +    char *notice  = dfnOpen(&src, &file, err, sizeof(err)) ? dfnLoadText(&src, &file, DfnTextNoticeE, err, sizeof(err)) : NULL;
    +    char *licence = notice ? dfnLoadText(&src, &file, DfnTextLicenceE, err, sizeof(err)) : NULL;
    +    char *text    = licence ? (char *)malloc(strlen(notice) + strlen(TEXT_EOL) + strlen(licence) + 1) : NULL;
    +
    +    if (text) {
    +        snprintf(text, strlen(notice) + strlen(TEXT_EOL) + strlen(licence) + 1, "%s" TEXT_EOL "%s", notice, licence);
    +    } else {
    +        dvxLog("Fonts: %s", err);
    +    }
    +
    +    free(notice);
    +    free(licence);
    +    return text;
    +}
    +
    +
    +BitmapFontT *dvxFontOpen(const char *family, int32_t pixelSize, uint32_t style) {
    +    return fontOpen(scopeOwner(), family, pixelSize, style);
    +}
    +
    +
    +// Every face added for owner forgotten (for OWNER_ALL, the added ones:
    +// the installed faces stay), then the families left with none
    +void dvxFontRemoveOwner(int32_t owner) {
    +    int32_t prev = memAsSystem();
    +
    +    for (int32_t i = (int32_t)arrlen(sFamilies) - 1; i > SYSTEM_FAMILY_INDEX; i--) {
    +        if (sFamilies[i].owner == owner) {
    +            familyFree(&sFamilies[i]);
    +            familyDropEmpty(i);
    +        }
    +    }
    +
    +    ownerDetach(owner, NULL);
    +    sViewOwner = OWNER_GONE;
    +    memRestore(prev);
    +}
    +
    +
    +// Read the FONTS folder again on the next call; unload the fonts no one
    +// holds (added families stay)
    +void dvxFontRescan(void) {
    +    int32_t prev = memAsSystem();
    +
    +    idleTrim(0);
    +    sScanned   = false;
    +    sViewOwner = OWNER_GONE;
    +    memRestore(prev);
    +}
    +
    +
    +BitmapFontT *dvxFontRetain(BitmapFontT *font) {
    +    FontEntryT *e = font ? (FontEntryT *)font->priv : NULL;
    +
    +    if (e) {
    +        e->refs++;
    +    }
    +
    +    return font;
    +}
    +
    +
    +// Fonts smoothed (anti-aliased) where their face has the size smoothed:
    +// the idle fonts are unloaded and the held ones built again in place --
    +// the fonts first, then the fonts with lines that copy them
    +void dvxFontSetSmooth(bool smooth) {
    +    if (smooth == sSmooth) {
    +        return;
    +    }
    +
    +    int32_t      prev = memAsSystem();
    +    FontEntryT **held = NULL;
    +
    +    sSmooth = smooth;
    +    scanFonts();
    +    idleTrim(0);
    +
    +    for (FontEntryT *e = sEntries; e; e = e->next) {
    +        arrput(held, e);
    +    }
    +
    +    for (int32_t i = 0; i < arrlen(held); i++) {
    +        if (!held[i]->base) {
    +            entryRebuild(held[i]);
    +        }
    +    }
    +
    +    for (int32_t i = 0; i < arrlen(held); i++) {
    +        if (held[i]->base) {
    +            entryLinesCopy(held[i], held[i]->font.style & FONT_STYLE_LINES);
    +        }
    +    }
    +
    +    arrfree(held);
    +    memRestore(prev);
    +}
    +
    +
    +int32_t dvxFontSizes(const char *family, uint32_t style, int32_t *sizes, int32_t max) {
    +    int32_t          owner = scopeOwner();
    +    const FontFaceT *mine  = NULL;
    +    const FontFaceT *base  = NULL;
    +    uint32_t         synth;
    +    int32_t          count = 0;
    +
    +    scanFonts();
    +
    +    if (!familyKnown(family, owner)) {
    +        return 0;
    +    }
    +
    +    if (!pickFaces(owner, family, style & FONT_STYLE_FACE, &mine, &base, &synth)) {
    +        if (max > 0) {
    +            sizes[0] = FONT_CHAR_HEIGHT;
    +        }
    +
    +        return 1;
    +    }
    +
    +    // Both faces' sizes, rising, each once
    +    int32_t i = 0;
    +    int32_t j = 0;
    +
    +    for (; (mine && i < mine->sizeCount) || (base && j < base->sizeCount); count++) {
    +        int32_t a = (mine && i < mine->sizeCount) ? mine->sizes[i] : INT32_MAX;
    +        int32_t b = (base && j < base->sizeCount) ? base->sizes[j] : INT32_MAX;
    +
    +        if (count < max) {
    +            sizes[count] = DVX_MIN(a, b);
    +        }
    +
    +        // The smaller taken (both when they match: listed once)
    +        if (a <= b) {
    +            i++;
    +        }
    +
    +        if (b <= a) {
    +            j++;
    +        }
    +    }
    +
    +    return count;
    +}
    +
    +
    +bool dvxFontSmooth(void) {
    +    return sSmooth;
    +}
    +
    +
    +// The font pick says (its face, NULL: the system font) in style for
    +// owner, from the size's gray strike when the pick is gray, built and
    +// added to the cache with one holder; NULL when out of memory.  A face
    +// that cannot be read is logged and the system font is built instead.
    +static FontEntryT *entryBuild(int32_t owner, const FontPickT *pick, uint32_t style) {
    +    FontEntryT    *e = (FontEntryT *)calloc(1, sizeof(FontEntryT));
    +    DfnStrikeDataT strike;
    +    BuildT         b;
    +
    +    if (!e) {
    +        return NULL;
    +    }
    +
    +    int32_t          size = pick->size;
    +    bool             gray = pick->gray;
    +    const FontFaceT *f    = pick->face;
    +
    +    memset(&b, 0, sizeof(b));
    +    memset(&strike, 0, sizeof(strike));
    +    snprintf(e->family, sizeof(e->family), "%s", pick->family);
    +    e->refs   = 1;
    +    e->owner  = owner;
    +    b.style   = style;
    +    b.synth   = style;
    +
    +    if (f) {
    +        DfnFileT   file;
    +        DfnSourceT src;
    +        char       err[DFN_ERR_MAX];
    +        int32_t    index = 0;
    +        bool       ok;
    +
    +        sourceDfn(f->src, &src);
    +        ok = dfnOpen(&src, &file, err, sizeof(err));
    +
    +        // The size's 1-bit or gray strike
    +        while (ok && index < file.header.strikeCount && (file.strikes[index].pixelSize != size || ((file.strikes[index].flags & DFN_STRIKE_GRAY2) != 0) != gray)) {
    +            index++;
    +        }
    +
    +        if (ok && dfnLoadStrike(&src, &file, index, &strike, err, sizeof(err))) {
    +            b.strike  = &strike;
    +            e->src    = f->src;
    +            e->src->refs++;
    +            b.bpp     = gray ? FONT_GRAY_BPP : 1;
    +            b.synth   = style & ~(uint32_t)f->style;
    +            b.boxDraw = (f->faceFlags & DFN_FACE_BOXDRAW) != 0;
    +            b.pitch   = (f->faceFlags & DFN_FACE_FIXED) ? strike.glyphs[CHAR_SPACE].advance : 0;
    +        } else {
    +            dvxLog("Fonts: %s; drawing %s with the system font", err, pick->family);
    +            snprintf(e->family, sizeof(e->family), "%s", FONT_SYSTEM_FAMILY);
    +            size = FONT_CHAR_HEIGHT;
    +        }
    +    }
    +
    +    // The plain system font draws through the fast fixed paths
    +    if (!b.strike && style == 0) {
    +        e->font        = dvxFont8x16;
    +        e->font.priv   = e;
    +        e->font.serial = ++sSerial;
    +        e->next        = sEntries;
    +        sEntries       = e;
    +        return e;
    +    }
    +
    +    if (!b.strike) {
    +        b.bpp = 1;
    +    }
    +
    +    b.ascent       = b.strike ? strike.info.ascent : FONT_SYS_ASCENT;
    +    b.lineHeight   = b.strike ? strike.info.lineHeight : FONT_CHAR_HEIGHT;
    +    b.pixelSize    = size;
    +    b.lineThick    = 1 + size / LINE_STEP;
    +    b.boldExtra    = 1 + size / SYNTH_BOLD_STEP;
    +    b.spaceW       = b.strike ? strike.glyphs[CHAR_SPACE].advance : FONT_CHAR_WIDTH;
    +    b.fallbackName = (b.strike && (f->faceFlags & DFN_FACE_FIXED)) ? FONT_FALLBACK_FIXED : FONT_FALLBACK_FAMILY;
    +
    +    if (!b.strike) {
    +        b.pitch = FONT_CHAR_WIDTH;
    +    }
    +
    +    // A made bold widens a fixed pitch: the face's box characters would
    +    // no longer join, so they are drawn to fill the wider cell
    +    if (b.synth & FONT_STYLE_BOLD) {
    +        b.spaceW  += b.boldExtra;
    +        b.boxDraw  = b.boxDraw || b.pitch != 0;
    +        b.pitch   += b.pitch ? b.boldExtra : 0;
    +    }
    +
    +    for (int32_t ch = 0; ch < DFN_CHAR_COUNT; ch++) {
    +        FontGlyphT     g;
    +        const uint8_t *bits = NULL;
    +        bool           has  = faceGlyph(&b, ch, &g, &bits);
    +
    +        // The box cell: the pitch, or the digit 0's width (built by now)
    +        if (ch == BOX_FIRST) {
    +            b.cellW = DVX_MAX(1, b.pitch ? b.pitch : e->glyphs[CHAR_ZERO].advance);
    +        }
    +
    +        if (isBox(ch) && (b.boxDraw || !has)) {
    +            boxGlyph(&b, ch, &g);
    +        } else if (has) {
    +            levelsFrom(&b, &g, bits, b.bpp);
    +
    +            if (!isBox(ch) && (b.synth & FONT_STYLE_BOLD)) {
    +                synthBold(&b, &g);
    +            }
    +
    +            if (!isBox(ch) && (b.synth & FONT_STYLE_ITALIC)) {
    +                synthItalic(&b, &g);
    +            }
    +        } else {
    +            // From the fallback family (opened the first time it is
    +            // needed; never for itself)
    +            if (!b.fallback && strcasecmp(pick->family, b.fallbackName) != 0) {
    +                b.fallback = fontOpen(owner, b.fallbackName, size, style);
    +            }
    +
    +            if (b.fallback && glyphView(b.fallback, ch, &g, &bits)) {
    +                levelsFrom(&b, &g, bits, FONT_BPP(b.fallback));
    +
    +                if (b.pitch) {
    +                    g.left    = (int8_t)(g.left + (b.pitch - g.advance) / 2);
    +                    g.advance = (uint8_t)b.pitch;
    +                }
    +            } else {
    +                memset(&g, 0, sizeof(g));
    +                g.advance = (uint8_t)(b.pitch ? b.pitch : b.spaceW);
    +            }
    +        }
    +
    +        entryPut(e, &b, ch, &g);
    +    }
    +
    +    entryFinish(e, &b);
    +    dvxFontClose(b.fallback);
    +    arrfree(b.px);
    +    arrfree(b.spare);
    +    dfnFreeStrike(&strike);
    +    e->next  = sEntries;
    +    sEntries = e;
    +    return e;
    +}
    +
    +
    +// The BitmapFontT of a built font
    +static void entryFinish(FontEntryT *e, const BuildT *b) {
    +    BitmapFontT      *f    = &e->font;
    +    const FontGlyphT *x    = &e->glyphs[CHAR_X];
    +    int32_t           xTop = (x->height > 0) ? x->top : b->ascent / 2;
    +    int32_t           desc = b->lineHeight - b->ascent;
    +
    +    f->charWidth  = b->pitch ? b->pitch : e->glyphs[CHAR_ZERO].advance;
    +    f->charHeight = b->lineHeight;
    +    f->firstChar  = 0;
    +    f->numChars   = DFN_CHAR_COUNT;
    +    f->glyphData  = e->bitmaps ? e->bitmaps : sNoBitmaps;
    +    f->glyphs     = e->glyphs;
    +    f->glyphAt    = e->glyphAt;
    +    f->ascent     = b->ascent;
    +    f->pixelSize  = b->pixelSize;
    +    f->style      = b->style;
    +    f->flags      = 0;
    +    f->lineThick  = b->lineThick;
    +    f->underlineY = DVX_MIN(b->ascent + DVX_MAX(1, desc / UNDERLINE_DROP_DIV), b->lineHeight - f->lineThick);
    +    f->strikeY    = b->ascent - (xTop + 1) / 2;
    +    f->priv       = e;
    +    f->serial     = ++sSerial;
    +
    +    if (b->bpp == FONT_GRAY_BPP) {
    +        f->flags |= FONT_FLAG_GRAY;
    +    }
    +
    +    if (!b->pitch) {
    +        f->flags |= FONT_FLAG_PROPORTIONAL;
    +    }
    +
    +    if (b->synth & FONT_STYLE_BOLD) {
    +        f->flags |= FONT_FLAG_SYNTH_BOLD;
    +    }
    +
    +    if (b->synth & FONT_STYLE_ITALIC) {
    +        f->flags |= FONT_FLAG_SYNTH_ITALIC;
    +    }
    +}
    +
    +
    +// Unload a font; a font with lines gives up the font it draws (which the
    +// caller's trim then unloads in turn when no one else holds it)
    +static void entryFree(FontEntryT *e) {
    +    if (e->base && --e->base->refs == 0) {
    +        e->base->lastUse = ++sUseClock;
    +    }
    +
    +    sourceRelease(e->src);
    +    arrfree(e->bitmaps);
    +    free(e);
    +}
    +
    +
    +// The font pick says at size with the lines of style, for owner: the
    +// font without them, holding it, with the line bits in its style; NULL
    +// when out of memory
    +static FontEntryT *entryLines(int32_t owner, const FontPickT *pick, uint32_t style) {
    +    BitmapFontT *base = fontOpen(owner, pick->family, pick->size, style & FONT_STYLE_FACE);
    +    FontEntryT  *e    = base ? (FontEntryT *)calloc(1, sizeof(FontEntryT)) : NULL;
    +
    +    if (!e) {
    +        dvxFontClose(base);
    +        return NULL;
    +    }
    +
    +    e->base  = (FontEntryT *)base->priv;
    +    e->refs  = 1;
    +    e->owner = owner;
    +    entryLinesCopy(e, style & FONT_STYLE_LINES);
    +    snprintf(e->family, sizeof(e->family), "%s", pick->family);
    +    e->next  = sEntries;
    +    sEntries = e;
    +    return e;
    +}
    +
    +
    +// A font with lines (e) as its base font with the lines added
    +static void entryLinesCopy(FontEntryT *e, uint32_t lines) {
    +    e->font         = e->base->font;
    +    e->font.style  |= lines;
    +    e->font.priv    = e;
    +    e->font.serial  = ++sSerial;
    +}
    +
    +
    +// Glyph g of character ch -- the glyph being made (b->px), none when its
    +// size is 0 -- into e at the font's depth
    +static void entryPut(FontEntryT *e, const BuildT *b, int32_t ch, const FontGlyphT *g) {
    +    uint32_t bytes = FONT_GLYPH_BYTES_BPP(g, b->bpp);
    +
    +    e->glyphs[ch]       = *g;
    +    e->glyphs[ch].flags = 0;
    +    e->glyphAt[ch]      = (uint32_t)arrlen(e->bitmaps);
    +
    +    if (bytes == 0) {
    +        e->glyphs[ch].width  = 0;
    +        e->glyphs[ch].height = 0;
    +        return;
    +    }
    +
    +    int32_t  rowBytes = FONT_ROW_BYTES_BPP(g->width, b->bpp);
    +    uint8_t *out      = arraddnptr(e->bitmaps, bytes);
    +
    +    memset(out, 0, bytes);
    +
    +    for (int32_t y = 0; y < g->height; y++) {
    +        uint8_t *row = out + y * rowBytes;
    +
    +        for (int32_t x = 0; x < g->width; x++) {
    +            uint8_t level = b->px[y * g->width + x];
    +
    +            if (b->bpp == FONT_GRAY_BPP) {
    +                row[x / FONT_GRAY_PER_BYTE] |= (uint8_t)(level << FONT_GRAY_SHIFT(x));
    +            } else if (level >= FONT_GRAY_HALF) {
    +                row[FONT_PIXEL_BYTE(x)] |= FONT_PIXEL_BIT(x);
    +            }
    +        }
    +    }
    +}
    +
    +
    +// A built font (e) built again in place, smoothed or not as its pick now
    +// says: the same BitmapFontT with new glyphs and a new serial.  Left as
    +// it is when it is already so, its family is gone (dvxFontRescan, or its
    +// program's fonts removed), or memory runs out.
    +static void entryRebuild(FontEntryT *e) {
    +    FontPickT pick;
    +
    +    if (e->owner == OWNER_GONE) {
    +        return;
    +    }
    +
    +    fontPick(e->owner, e->family, e->font.pixelSize, e->font.style & FONT_STYLE_FACE, &pick);
    +
    +    if (strcasecmp(pick.family, e->family) != 0 || ((e->font.flags & FONT_FLAG_GRAY) != 0) == pick.gray) {
    +        return;
    +    }
    +
    +    FontEntryT *n = entryBuild(e->owner, &pick, e->font.style);
    +
    +    if (!n) {
    +        return;
    +    }
    +
    +    // entryBuild put n first; e takes its glyphs and the source they came from
    +    sEntries = n->next;
    +    sourceRelease(e->src);
    +    e->src = n->src;
    +    arrfree(e->bitmaps);
    +    memcpy(e->glyphs, n->glyphs, sizeof(e->glyphs));
    +    memcpy(e->glyphAt, n->glyphAt, sizeof(e->glyphAt));
    +    e->bitmaps      = n->bitmaps;
    +    e->font         = n->font;
    +    e->font.glyphs  = e->glyphs;
    +    e->font.glyphAt = e->glyphAt;
    +    e->font.priv    = e;
    +    free(n);
    +}
    +
    +
    +// The font at src added as a face of its family for owner (OWNER_ALL:
    +// for every program).  False, logged, when it cannot be read, calls its
    +// family "System", is a second face of its family and style for owner, or
    +// memory runs out.
    +static bool faceAdd(int32_t owner, const DfnSourceT *src) {
    +    DfnFileT file;
    +    char     err[DFN_ERR_MAX];
    +
    +    scanFonts();
    +
    +    if (owner < OWNER_ALL || !dfnOpen(src, &file, err, sizeof(err))) {
    +        dvxLog("Fonts: %s", (owner < OWNER_ALL) ? "a font added for no program; skipped" : err);
    +        return false;
    +    }
    +
    +    if (!file.header.family[0] || strcasecmp(file.header.family, FONT_SYSTEM_FAMILY) == 0) {
    +        dvxLog("Fonts: %s calls its family %s; skipped", src->name ? src->name : "a font", file.header.family[0] ? FONT_SYSTEM_FAMILY : "nothing");
    +        return false;
    +    }
    +
    +    int32_t    prev  = memAsSystem();
    +    int32_t    fam   = familyMake(file.header.family, owner);
    +    uint32_t   style = file.header.style & FONT_STYLE_FACE;
    +    FontFaceT *face  = &sFamilies[fam].faces[style];
    +    bool       ok    = face->sizeCount == 0;
    +
    +    if (!ok) {
    +        dvxLog("Fonts: %s is a second %s face of style %u; skipped", src->name ? src->name : "a font", file.header.family, (unsigned)style);
    +    } else {
    +        face->src = sourceMake(src);
    +        ok        = face->src != NULL;
    +
    +        if (ok) {
    +            faceFill(face, &file);
    +            ownerDetach(owner, file.header.family);
    +        }
    +    }
    +
    +    familyDropEmpty(fam);
    +    sViewOwner = OWNER_GONE;
    +    memRestore(prev);
    +    return ok;
    +}
    +
    +
    +// face's sizes, smoothed sizes, style and flags from its file's header
    +// and strike table (each size once; a gray strike follows its size's
    +// 1-bit one)
    +static void faceFill(FontFaceT *face, const DfnFileT *file) {
    +    face->style     = file->header.style & FONT_STYLE_FACE;
    +    face->faceFlags = file->header.faceFlags;
    +    face->sizeCount = 0;
    +    face->grayMask  = 0;
    +
    +    for (int32_t i = 0; i < file->header.strikeCount; i++) {
    +        if (file->strikes[i].flags & DFN_STRIKE_GRAY2) {
    +            face->grayMask |= (face->sizeCount > 0) ? 1u << (face->sizeCount - 1) : 0;
    +        } else {
    +            face->sizes[face->sizeCount++] = (uint8_t)file->strikes[i].pixelSize;
    +        }
    +    }
    +}
    +
    +
    +// A face's source given up; the face left empty
    +static void faceFree(FontFaceT *face) {
    +    sourceRelease(face->src);
    +    memset(face, 0, sizeof(*face));
    +}
    +
    +
    +// Character ch of the face a font is built from; false when it lacks it
    +static bool faceGlyph(const BuildT *b, int32_t ch, FontGlyphT *g, const uint8_t **bits) {
    +    if (!b->strike) {
    +        return glyphView(&dvxFont8x16, ch, g, bits);
    +    }
    +
    +    *g    = b->strike->glyphs[ch];
    +    *bits = b->strike->bitmaps ? b->strike->bitmaps + b->strike->bitmapAt[ch] : NULL;
    +    return !(g->flags & FONT_GLYPH_MISSING);
    +}
    +
    +
    +// Whether face (NULL: the system font) at size is built smoothed:
    +// smoothing is on and the face has the size smoothed too
    +static bool faceGray(const FontFaceT *face, int32_t size) {
    +    int32_t i = face ? sizeIndex(face, size) : -1;
    +
    +    return sSmooth && i >= 0 && (face->grayMask & (1u << i)) != 0;
    +}
    +
    +
    +// Family fam taken out when it has no face (never the system font's)
    +static void familyDropEmpty(int32_t fam) {
    +    for (int32_t i = 0; i < FACE_STYLES; i++) {
    +        if (sFamilies[fam].faces[i].sizeCount) {
    +            return;
    +        }
    +    }
    +
    +    if (fam != SYSTEM_FAMILY_INDEX) {
    +        arrdel(sFamilies, fam);
    +    }
    +}
    +
    +
    +// The index of owner's family name (any case); NO_FAMILY for none.
    +// NULL, "" and "System" are the system font's (OWNER_ALL).
    +static int32_t familyFind(const char *name, int32_t owner) {
    +    if (!name || !name[0] || strcasecmp(name, FONT_SYSTEM_FAMILY) == 0) {
    +        return (owner == OWNER_ALL) ? SYSTEM_FAMILY_INDEX : NO_FAMILY;
    +    }
    +
    +    for (int32_t i = SYSTEM_FAMILY_INDEX + 1; i < arrlen(sFamilies); i++) {
    +        if (sFamilies[i].owner == owner && strcasecmp(sFamilies[i].name, name) == 0) {
    +            return i;
    +        }
    +    }
    +
    +    return NO_FAMILY;
    +}
    +
    +
    +// fam's added faces given up and left empty (its installed ones stay)
    +static void familyFree(FontFamilyT *fam) {
    +    for (int32_t i = 0; i < FACE_STYLES; i++) {
    +        if (fam->faces[i].src && !fam->faces[i].installed) {
    +            faceFree(&fam->faces[i]);
    +        }
    +    }
    +}
    +
    +
    +// Whether owner sees a family name (the system font's names included)
    +static bool familyKnown(const char *name, int32_t owner) {
    +    return familyFind(name, OWNER_ALL) != NO_FAMILY || (owner != OWNER_ALL && familyFind(name, owner) != NO_FAMILY);
    +}
    +
    +
    +// The index of owner's family name, made (with no faces) when there is
    +// none
    +static int32_t familyMake(const char *name, int32_t owner) {
    +    int32_t     fam = familyFind(name, owner);
    +    FontFamilyT add;
    +
    +    if (fam != NO_FAMILY) {
    +        return fam;
    +    }
    +
    +    memset(&add, 0, sizeof(add));
    +    snprintf(add.name, sizeof(add.name), "%s", name);
    +    add.owner = owner;
    +    arrput(sFamilies, add);
    +    return (int32_t)arrlen(sFamilies) - 1;
    +}
    +
    +
    +// dvxFontOpen for owner's view of the families
    +static BitmapFontT *fontOpen(int32_t owner, const char *family, int32_t pixelSize, uint32_t style) {
    +    FontPickT pick;
    +
    +    scanFonts();
    +    style &= FONT_STYLE_FACE | FONT_STYLE_LINES;
    +    fontPick(owner, family, pixelSize, style & FONT_STYLE_FACE, &pick);
    +
    +    for (FontEntryT *e = sEntries; e; e = e->next) {
    +        if (e->owner == owner && e->font.pixelSize == pick.size && e->font.style == style && ((e->font.flags & FONT_FLAG_GRAY) != 0) == pick.gray && strcasecmp(e->family, pick.family) == 0) {
    +            e->refs++;
    +            return &e->font;
    +        }
    +    }
    +
    +    int32_t     prev = memAsSystem();
    +    FontEntryT *e    = (style & FONT_STYLE_LINES) ? entryLines(owner, &pick, style) : entryBuild(owner, &pick, style);
    +
    +    memRestore(prev);
    +    return e ? &e->font : NULL;
    +}
    +
    +
    +// What owner's font of family at pixelSize in style (face bits only) is
    +// built from: the size nearest pixelSize the style's faces have (owner's
    +// and the installed one together), from owner's face when it has that
    +// size; the system font for a family owner does not see
    +static void fontPick(int32_t owner, const char *family, int32_t pixelSize, uint32_t style, FontPickT *pick) {
    +    const FontFaceT *mine = NULL;
    +    const FontFaceT *base = NULL;
    +
    +    memset(pick, 0, sizeof(*pick));
    +    pick->size  = FONT_CHAR_HEIGHT;
    +    pick->synth = style;
    +    snprintf(pick->family, sizeof(pick->family), "%s", FONT_SYSTEM_FAMILY);
    +
    +    if (!pickFaces(owner, family, style, &mine, &base, &pick->synth)) {
    +        return;
    +    }
    +
    +    int32_t inst = familyFind(family, OWNER_ALL);
    +
    +    pick->size = nearestSize(mine, base, pixelSize);
    +    pick->face = (mine && sizeIndex(mine, pick->size) >= 0) ? mine : base;
    +    pick->gray = faceGray(pick->face, pick->size);
    +    snprintf(pick->family, sizeof(pick->family), "%s", sFamilies[(inst != NO_FAMILY) ? inst : familyFind(family, owner)].name);
    +}
    +
    +
    +// Character ch of font as a loaded glyph and its bitmap: a loaded font's
    +// own, or the system font's 8-pixel cell (one byte per row, the same
    +// layout); false for a character it has no bitmap for
    +static bool glyphView(const BitmapFontT *font, int32_t ch, FontGlyphT *g, const uint8_t **bits) {
    +    int32_t idx = ch - font->firstChar;
    +
    +    if (idx < 0 || idx >= font->numChars) {
    +        return false;
    +    }
    +
    +    if (font->glyphs) {
    +        *g    = font->glyphs[idx];
    +        *bits = font->glyphData + font->glyphAt[idx];
    +        return g->width > 0 || ch == CHAR_SPACE;
    +    }
    +
    +    g->advance = (uint8_t)font->charWidth;
    +    g->width   = (uint8_t)font->charWidth;
    +    g->height  = (uint8_t)font->charHeight;
    +    g->left    = 0;
    +    g->top     = (int8_t)font->ascent;
    +    g->flags   = 0;
    +    *bits      = font->glyphData + idx * font->charHeight;
    +    return true;
    +}
    +
    +
    +// Unload the fonts no one holds, least recently given up first, until
    +// at most keep are left
    +static void idleTrim(int32_t keep) {
    +    for (;;) {
    +        FontEntryT **oldest = NULL;
    +        int32_t      idle   = 0;
    +
    +        for (FontEntryT **p = &sEntries; *p; p = &(*p)->next) {
    +            if ((*p)->refs == 0) {
    +                idle++;
    +
    +                if (!oldest || (*p)->lastUse < (*oldest)->lastUse) {
    +                    oldest = p;
    +                }
    +            }
    +        }
    +
    +        if (idle <= keep) {
    +            return;
    +        }
    +
    +        FontEntryT *e = *oldest;
    +
    +        *oldest = e->next;
    +        entryFree(e);
    +    }
    +}
    +
    +
    +// Box drawing, block or shade character
    +static bool isBox(int32_t ch) {
    +    return ch >= BOX_FIRST && ch <= BOX_LAST;
    +}
    +
    +
    +// Raise [x0, x1) x [y0, y1) of a w x h level map to the full level
    +// (clipped)
    +static void levelRect(uint8_t *px, int32_t w, int32_t h, int32_t x0, int32_t y0, int32_t x1, int32_t y1) {
    +    x0 = DVX_MAX(x0, 0);
    +    y0 = DVX_MAX(y0, 0);
    +    x1 = DVX_MIN(x1, w);
    +    y1 = DVX_MIN(y1, h);
    +
    +    for (int32_t y = y0; y < y1; y++) {
    +        memset(px + y * w + x0, FONT_GRAY_FULL, (size_t)DVX_MAX(x1 - x0, 0));
    +    }
    +}
    +
    +
    +// Glyph g's bitmap bits (bpp bits per pixel) as the glyph being made, one
    +// level per pixel (a 1-bit pixel at the full level)
    +static void levelsFrom(BuildT *b, const FontGlyphT *g, const uint8_t *bits, int32_t bpp) {
    +    int32_t  rowBytes = FONT_ROW_BYTES_BPP(g->width, bpp);
    +    uint8_t *px       = workArea(&b->px, g->width * g->height);
    +
    +    for (int32_t y = 0; y < g->height; y++) {
    +        const uint8_t *row = bits + y * rowBytes;
    +
    +        for (int32_t x = 0; x < g->width; x++) {
    +            px[y * g->width + x] = (uint8_t)((bpp == FONT_GRAY_BPP) ? FONT_GRAY_LEVEL(row, x) : ((row[FONT_PIXEL_BYTE(x)] & FONT_PIXEL_BIT(x)) ? FONT_GRAY_FULL : 0));
    +        }
    +    }
    +}
    +
    +
    +// Make the system (app 0) the owner of what is allocated until
    +// memRestore(the value returned)
    +static int32_t memAsSystem(void) {
    +    int32_t prev = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
    +
    +    if (dvxMemAppIdPtr) {
    +        *dvxMemAppIdPtr = 0;
    +    }
    +
    +    return prev;
    +}
    +
    +
    +static void memRestore(int32_t prev) {
    +    if (dvxMemAppIdPtr) {
    +        *dvxMemAppIdPtr = prev;
    +    }
    +}
    +
    +
    +static int nameCompare(const void *a, const void *b) {
    +    return strcasecmp(*(const char *const *)a, *(const char *const *)b);
    +}
    +
    +
    +// The size of the two faces (either may be NULL) nearest pixelSize, the
    +// smaller of two as near
    +static int32_t nearestSize(const FontFaceT *a, const FontFaceT *b, int32_t pixelSize) {
    +    const FontFaceT *faces[] = { a, b };
    +    int32_t          best    = -1;
    +
    +    for (int32_t f = 0; f < DVX_ARRAY_LEN(faces); f++) {
    +        for (int32_t i = 0; faces[f] && i < faces[f]->sizeCount; i++) {
    +            int32_t size = faces[f]->sizes[i];
    +            int32_t gap  = abs(size - pixelSize);
    +            int32_t was  = abs(best - pixelSize);
    +
    +            if (best < 0 || gap < was || (gap == was && size < best)) {
    +                best = size;
    +            }
    +        }
    +    }
    +
    +    return best;
    +}
    +
    +
    +// The fonts built for owner from family (NULL, or a fallback family,
    +// whose glyphs any font may hold: all of owner's), whose faces just
    +// changed, no longer found: the ones no one holds unloaded, the held
    +// ones kept as they are (OWNER_GONE)
    +static void ownerDetach(int32_t owner, const char *family) {
    +    bool all = !family || strcasecmp(family, FONT_FALLBACK_FAMILY) == 0 || strcasecmp(family, FONT_FALLBACK_FIXED) == 0;
    +
    +    for (FontEntryT *e = sEntries; e; e = e->next) {
    +        if (e->owner == owner && (all || strcasecmp(e->family, family) == 0)) {
    +            e->owner = OWNER_GONE;
    +        }
    +    }
    +
    +    // Unloading a font with lines can leave the font it draws unheld
    +    // (entryFree only lets go of it: the list stays as it is)
    +    for (bool freed = true; freed; ) {
    +        FontEntryT **p = &sEntries;
    +
    +        freed = false;
    +
    +        while (*p) {
    +            FontEntryT *e = *p;
    +
    +            if (e->owner == OWNER_GONE && e->refs == 0) {
    +                *p    = e->next;
    +                freed = true;
    +                entryFree(e);
    +            } else {
    +                p = &e->next;
    +            }
    +        }
    +    }
    +}
    +
    +
    +// The faces of family a style is drawn from for owner: owner's (*mine)
    +// and the installed one (*base), either NULL, and the style bits made
    +// here (*synth); false when there are none (the system font)
    +static bool pickFaces(int32_t owner, const char *family, uint32_t style, const FontFaceT **mine, const FontFaceT **base, uint32_t *synth) {
    +    int32_t inst = familyFind(family, OWNER_ALL);
    +    int32_t own  = (owner != OWNER_ALL) ? familyFind(family, owner) : NO_FAMILY;
    +
    +    for (int32_t i = 0; i < FACE_STYLES; i++) {
    +        uint32_t want = sFaceOrder[style][i];
    +
    +        *mine = (own != NO_FAMILY && sFamilies[own].faces[want].sizeCount) ? &sFamilies[own].faces[want] : NULL;
    +        *base = (inst != NO_FAMILY && sFamilies[inst].faces[want].sizeCount) ? &sFamilies[inst].faces[want] : NULL;
    +
    +        if (*mine || *base) {
    +            *synth = style & ~want;
    +            return true;
    +        }
    +    }
    +
    +    *synth = style;
    +    return false;
    +}
    +
    +
    +// The installed families from the .DFN files in DVX_FONTS_DIR (once until
    +// dvxFontRescan), after the system font and with the added families
    +// kept.  A file that cannot be read, or a second file for a family and
    +// style (after the first in alphabetical order), is logged and skipped.
    +static void scanFonts(void) {
    +    if (sScanned) {
    +        return;
    +    }
    +
    +    int32_t prev = memAsSystem();
    +    char    cwd[DVX_MAX_PATH];
    +    char    folder[DVX_MAX_PATH];
    +
    +    // The folder by its full name, so a program that changes the current
    +    // folder later still finds the fonts (and their licences)
    +    if (!getcwd(cwd, sizeof(cwd)) || !platformPathJoin(folder, sizeof(folder), cwd, DVX_FONTS_DIR)) {
    +        snprintf(folder, sizeof(folder), "%s", DVX_FONTS_DIR);
    +    }
    +
    +    char **names = dvxReadDir(folder);
    +
    +    sScanned   = true;
    +    sViewOwner = OWNER_GONE;
    +
    +    // The installed faces forgotten (the added ones stay), then the
    +    // families left with none
    +    for (int32_t i = (int32_t)arrlen(sFamilies) - 1; i > SYSTEM_FAMILY_INDEX; i--) {
    +        for (int32_t s = 0; s < FACE_STYLES; s++) {
    +            if (sFamilies[i].faces[s].installed) {
    +                faceFree(&sFamilies[i].faces[s]);
    +            }
    +        }
    +
    +        familyDropEmpty(i);
    +    }
    +
    +    if (arrlen(sFamilies) == 0) {
    +        FontFamilyT sys;
    +
    +        memset(&sys, 0, sizeof(sys));
    +        snprintf(sys.name, sizeof(sys.name), "%s", FONT_SYSTEM_FAMILY);
    +        arrput(sFamilies, sys);
    +    }
    +
    +    if (names) {
    +        qsort(names, (size_t)arrlen(names), sizeof(names[0]), nameCompare);
    +    }
    +
    +    for (int32_t n = 0; names && n < arrlen(names); n++) {
    +        const char *dot = strrchr(names[n], '.');
    +        char        path[DVX_MAX_PATH];
    +        char        err[DFN_ERR_MAX];
    +        DfnFileT    file;
    +        DfnSourceT  src;
    +
    +        if (!dot || strcasecmp(dot, DFN_EXT) != 0) {
    +            continue;
    +        }
    +
    +        if (!platformPathJoin(path, sizeof(path), folder, names[n])) {
    +            continue;
    +        }
    +
    +        dfnSourceFile(&src, path);
    +
    +        if (!dfnOpen(&src, &file, err, sizeof(err))) {
    +            dvxLog("Fonts: %s", err);
    +            continue;
    +        }
    +
    +        if (strcasecmp(file.header.family, FONT_SYSTEM_FAMILY) == 0) {
    +            dvxLog("Fonts: %s calls its family %s, the system font's name; skipped", path, FONT_SYSTEM_FAMILY);
    +            continue;
    +        }
    +
    +        int32_t    fam   = familyMake(file.header.family, OWNER_ALL);
    +        uint32_t   style = file.header.style & FONT_STYLE_FACE;
    +        FontFaceT *face  = &sFamilies[fam].faces[style];
    +
    +        if (face->sizeCount) {
    +            dvxLog("Fonts: %s is a second %s face of style %u; skipped", path, file.header.family, (unsigned)style);
    +            continue;
    +        }
    +
    +        face->src = sourceMake(&src);
    +
    +        if (face->src) {
    +            face->installed = true;
    +            faceFill(face, &file);
    +        }
    +
    +        familyDropEmpty(fam);
    +    }
    +
    +    dvxReadDirFree(names);
    +    memRestore(prev);
    +}
    +
    +
    +// Whose view of the families the running program has: its own when it
    +// has families added for it, else everyone's
    +static int32_t scopeOwner(void) {
    +    int32_t app = dvxMemAppIdPtr ? *dvxMemAppIdPtr : OWNER_ALL;
    +
    +    for (int32_t i = SYSTEM_FAMILY_INDEX + 1; app != OWNER_ALL && i < arrlen(sFamilies); i++) {
    +        if (sFamilies[i].owner == app) {
    +            return app;
    +        }
    +    }
    +
    +    return OWNER_ALL;
    +}
    +
    +
    +// The index of size among face's sizes; -1 when it lacks it
    +static int32_t sizeIndex(const FontFaceT *face, int32_t size) {
    +    for (int32_t i = 0; i < face->sizeCount; i++) {
    +        if (face->sizes[i] == size) {
    +            return i;
    +        }
    +    }
    +
    +    return -1;
    +}
    +
    +
    +// src as the reader takes it
    +static void sourceDfn(const FontSourceT *src, DfnSourceT *out) {
    +    memset(out, 0, sizeof(*out));
    +    out->name   = src->path;
    +    out->data   = src->data;
    +    out->base   = src->base;
    +    out->length = src->length;
    +}
    +
    +
    +// A source of the font at dfn, held once (a font in memory copied); NULL
    +// when out of memory
    +static FontSourceT *sourceMake(const DfnSourceT *dfn) {
    +    FontSourceT *src = (FontSourceT *)calloc(1, sizeof(FontSourceT));
    +
    +    if (!src) {
    +        return NULL;
    +    }
    +
    +    src->refs   = 1;
    +    src->path   = strdup(dfn->name ? dfn->name : "");
    +    src->data   = dfn->data ? (uint8_t *)malloc(dfn->length) : NULL;
    +    src->base   = dfn->base;
    +    src->length = dfn->length;
    +
    +    if (!src->path || (dfn->data && !src->data)) {
    +        sourceRelease(src);
    +        return NULL;
    +    }
    +
    +    if (src->data) {
    +        memcpy(src->data, dfn->data, dfn->length);
    +    }
    +
    +    return src;
    +}
    +
    +
    +// Give up one hold of src (NULL: nothing); the last frees it
    +static void sourceRelease(FontSourceT *src) {
    +    if (src && --src->refs == 0) {
    +        free(src->path);
    +        free(src->data);
    +        free(src);
    +    }
    +}
    +
    +
    +// Thicken the glyph being made by the build's bold columns: each pixel's
    +// level repeated that many times to its right (the stronger one kept)
    +static void synthBold(BuildT *b, FontGlyphT *g) {
    +    int32_t w = g->width + b->boldExtra;
    +
    +    if (g->width == 0 || w > UINT8_MAX) {
    +        g->advance = (uint8_t)DVX_MIN(g->advance + b->boldExtra, UINT8_MAX);
    +        return;
    +    }
    +
    +    uint8_t *out = workArea(&b->spare, w * g->height);
    +
    +    for (int32_t y = 0; y < g->height; y++) {
    +        for (int32_t x = 0; x < g->width; x++) {
    +            uint8_t level = b->px[y * g->width + x];
    +
    +            for (int32_t k = 0; k <= b->boldExtra; k++) {
    +                uint8_t *dst = &out[y * w + x + k];
    +
    +                *dst = DVX_MAX(*dst, level);
    +            }
    +        }
    +    }
    +
    +    uint8_t *done = b->spare;
    +
    +    b->spare   = b->px;
    +    b->px      = done;
    +    g->width   = (uint8_t)w;
    +    g->advance = (uint8_t)(g->advance + b->boldExtra);
    +}
    +
    +
    +// Slant the glyph being made: each row moved right one column per
    +// SYNTH_SLANT rows above the baseline (left below it).  The advance
    +// stays: an italic leans over its neighbor, as made italics do.
    +static void synthItalic(BuildT *b, FontGlyphT *g) {
    +    if (g->width == 0) {
    +        return;
    +    }
    +
    +    int32_t lowest  = g->top - g->height;                 // rows above the baseline of the last row
    +    int32_t minMove = (lowest >= 0) ? lowest / SYNTH_SLANT : -((-lowest + SYNTH_SLANT - 1) / SYNTH_SLANT);
    +    int32_t maxMove = (g->top - 1 >= 0) ? (g->top - 1) / SYNTH_SLANT : -((1 - g->top + SYNTH_SLANT - 1) / SYNTH_SLANT);
    +    int32_t w       = g->width + maxMove - minMove;
    +
    +    if (w > UINT8_MAX) {
    +        return;
    +    }
    +
    +    uint8_t *out = workArea(&b->spare, w * g->height);
    +
    +    for (int32_t y = 0; y < g->height; y++) {
    +        int32_t rel  = g->top - 1 - y;
    +        int32_t move = (rel >= 0) ? rel / SYNTH_SLANT : -((-rel + SYNTH_SLANT - 1) / SYNTH_SLANT);
    +
    +        memcpy(out + y * w + move - minMove, b->px + y * g->width, (size_t)g->width);
    +    }
    +
    +    uint8_t *done = b->spare;
    +
    +    b->spare = b->px;
    +    b->px    = done;
    +    g->left  = (int8_t)(g->left + minMove);
    +    g->width = (uint8_t)w;
    +}
    +
    +
    +static int viewCompare(const void *a, const void *b) {
    +    return strcasecmp(*(const char *const *)a, *(const char *const *)b);
    +}
    +
    +
    +// sView made owner's: the installed families' names and those of owner's
    +// own a family installed does not already name, "System" first, the rest
    +// alphabetical
    +static void viewFor(int32_t owner) {
    +    scanFonts();
    +
    +    if (sViewOwner == owner) {
    +        return;
    +    }
    +
    +    int32_t prev = memAsSystem();
    +
    +    arrsetlen(sView, 0);
    +
    +    for (int32_t i = 0; i < arrlen(sFamilies); i++) {
    +        const FontFamilyT *fam = &sFamilies[i];
    +
    +        if (fam->owner == OWNER_ALL || (fam->owner == owner && familyFind(fam->name, OWNER_ALL) == NO_FAMILY)) {
    +            arrput(sView, fam->name);
    +        }
    +    }
    +
    +    qsort(sView + 1, (size_t)(arrlen(sView) - 1), sizeof(sView[0]), viewCompare);
    +    sViewOwner = owner;
    +    memRestore(prev);
    +}
    +
    +
    +// *buf (an stb_ds array) bytes long and cleared, for a glyph being made
    +static uint8_t *workArea(uint8_t **buf, int32_t bytes) {
    +    arrsetlen(*buf, bytes);
    +
    +    if (bytes > 0) {
    +        memset(*buf, 0, (size_t)bytes);
    +    }
    +
    +    return *buf;
    +}
    diff --git a/src/libs/kpunch/libdvx/dvxText.h b/src/libs/kpunch/libdvx/dvxText.h
    new file mode 100644
    index 00000000..0f8d3624
    --- /dev/null
    +++ b/src/libs/kpunch/libdvx/dvxText.h
    @@ -0,0 +1,147 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// dvxText.h  -- fonts: the families in the FONTS folder, the ones added
    +// for a program, and the system font
    +//
    +// A family is one name ("DejaVu Sans") with up to four faces (regular,
    +// bold, italic, bold italic), each a .DFN file holding the face at the
    +// pixel sizes it was made at (dvxFontFmt.h; made on the build host by
    +// mkfont from TrueType, Type 1, and bitmap fonts).  "System" is the
    +// built-in 8x16 font, always there.
    +//
    +// dvxFontOpen gives a BitmapFontT (dvxTypes.h) every text call draws and
    +// measures with.  It never scales: it picks the size the face has that is
    +// nearest the one asked for.  A style the family has no face for is made
    +// from the nearest face: bold by thickening, italic by slanting.  Every
    +// character of code page 437 draws: what a face lacks is filled in when
    +// it loads -- box drawing, block, and shade characters drawn to fill the
    +// character cell (also in place of a face's own when its file asks), the
    +// rest taken from FONT_FALLBACK_FAMILY (FONT_FALLBACK_FIXED for a
    +// fixed-pitch face) at the nearest size, or from the system font.
    +//
    +// Fonts are shared and counted: opening the same family, size, and style
    +// again gives the same font; dvxFontClose gives one up.  A font with lines
    +// shares the glyphs of the same font without them.  A few fonts no
    +// one holds stay loaded so reopening them is quick.  The family list is
    +// read from FONTS (DVX_FONTS_DIR, beside DVX.INI's CONFIG folder) the
    +// first time it is needed.
    +//
    +// A program can carry fonts of its own -- font resources in its file (the
    +// shell adds them when it loads the program), .DFN files, or fonts in
    +// memory -- added for it alone (its owner: its app id).  It sees the
    +// installed families and its own; where it has a family that is
    +// installed too, its faces and sizes come first and the installed family
    +// fills in the styles and sizes it lacks.  Other programs never see them.
    +// Every call below works for the running program (the app id the shell
    +// sets while a program's code runs).
    +
    +#ifndef DVX_TEXT_H
    +#define DVX_TEXT_H
    +
    +#include "dvxTypes.h"
    +
    +#include 
    +#include 
    +
    +#define DVX_FONTS_DIR         "FONTS"
    +#define FONT_SYSTEM_FAMILY    "System"
    +#define FONT_FALLBACK_FAMILY  "DejaVu Sans"         // fills what a proportional face lacks
    +#define FONT_FALLBACK_FIXED   "DejaVu Sans Mono"    // fills what a fixed-pitch face lacks
    +
    +// The font of family (NULL, "", or a family there is none of: the system
    +// font) at the size nearest pixelSize, in style: FONT_STYLE_BOLD and / or
    +// FONT_STYLE_ITALIC pick the face, FONT_STYLE_UNDERLINE and / or
    +// FONT_STYLE_STRIKE are lines every text call then draws with it.
    +// Returns NULL only when out of memory.  Close it with dvxFontClose.
    +BitmapFontT *dvxFontOpen(const char *family, int32_t pixelSize, uint32_t style);
    +
    +// Give up a font dvxFontOpen gave.  NULL and fonts not from dvxFontOpen
    +// (dvxFont8x16, the application context's font) are ignored.
    +void dvxFontClose(BitmapFontT *font);
    +
    +// The number of font families, the system font's included.
    +int32_t dvxFontFamilyCount(void);
    +
    +// Family index (0 .. dvxFontFamilyCount() - 1): "System" first, then the
    +// others in alphabetical order.  NULL for an index out of range.
    +const char *dvxFontFamilyName(int32_t index);
    +
    +// The family a font was opened from ("System" for the system font and
    +// fonts not from dvxFontOpen).
    +const char *dvxFontFamilyOf(const BitmapFontT *font);
    +
    +// True when every face of family is fixed-pitch (the system font is).
    +bool dvxFontFamilyFixed(const char *family);
    +
    +// The pixel sizes family has in style (those of the face the style is
    +// drawn from), smallest first, into sizes (at most max).  Returns how
    +// many there are (which may be more than max); 0 for no such family.
    +int32_t dvxFontSizes(const char *family, uint32_t style, int32_t *sizes, int32_t max);
    +
    +// Hold font once more (each hold is given up with dvxFontClose), for
    +// keeping a font someone else opened.  Returns font; fonts not from
    +// dvxFontOpen are returned as they are.
    +BitmapFontT *dvxFontRetain(BitmapFontT *font);
    +
    +// Smooth (anti-alias) fonts where their face has the size smoothed (a
    +// gray strike), or stop.  The fonts open are built again in place: the
    +// same pointers, new glyphs, and new serials (callers lay text out
    +// again).
    +// Off at first; dvxSetFontSmoothing (dvxApp.h) sets it from the user's
    +// choice and the screen's depth.  dvxFontSmooth says which it is.
    +void dvxFontSetSmooth(bool smooth);
    +bool dvxFontSmooth(void);
    +
    +// Read the FONTS folder again on the next call (after fonts were added or
    +// removed); the fonts no one holds are unloaded.  Open fonts stay valid.
    +// Families added for programs stay.
    +void dvxFontRescan(void);
    +
    +// Add the .DFN file at path as a face of its family for program owner
    +// (0: for every program).  False, logged, when it cannot be read, calls
    +// its family "System", or owner has a face of that family and style
    +// already.
    +bool dvxFontAddFile(int32_t owner, const char *path);
    +
    +// The same for a .DFN in memory, length bytes (copied), named name in
    +// the log.
    +bool dvxFontAddMemory(int32_t owner, const void *data, uint32_t length, const char *name);
    +
    +// Add every font resource (DVX_RES_FONT, dvxRes.h) of the file at path
    +// (a program, library, or resource file) for owner.  Returns how many
    +// were added.
    +int32_t dvxFontAddResources(int32_t owner, const char *path);
    +
    +// Forget every face added for owner (the shell does when a program
    +// ends; for 0, the faces added for every program -- installed ones
    +// stay).  Its fonts no one holds are unloaded; held ones stay as they
    +// are.
    +void dvxFontRemoveOwner(int32_t owner);
    +
    +// The texts that go with the face font was built from: its notice (the
    +// family, what it was converted from, and the copyright notices) and its
    +// licence, a blank line between them, CR LF line ends.  malloc'd: the
    +// caller frees it.  NULL for the system font, and when it cannot be read.
    +char *dvxFontLicence(const BitmapFontT *font);
    +
    +#endif // DVX_TEXT_H
    diff --git a/src/libs/kpunch/libdvx/dvxTextBas.c b/src/libs/kpunch/libdvx/dvxTextBas.c
    new file mode 100644
    index 00000000..9c31543b
    --- /dev/null
    +++ b/src/libs/kpunch/libdvx/dvxTextBas.c
    @@ -0,0 +1,194 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// dvxTextBas.c  -- DVX BASIC entry points of the font functions
    +// (dvxText.h; src/include/basic/font.bas)
    +//
    +// Called through DECLARE LIBRARY, so the signatures use only BASIC-
    +// marshalable types: strings and LONG in, LONG or STRING out, True (-1) /
    +// False (0) for yes / no.  A font is named by family, size and style on
    +// every call; the font cache makes the repeated opening cheap, and a
    +// program holds nothing that must be closed.  Fonts a program adds are
    +// its own (the running program's app id); they go when it ends.
    +
    +#include "dvxText.h"
    +#include "dvxDraw.h"
    +#include "dvxFontFmt.h"
    +#include "dvxMem.h"
    +
    +#include 
    +#include 
    +
    +#define BAS_TRUE  (-1)
    +#define BAS_FALSE 0
    +
    +// What a measuring call reads from the font
    +typedef enum {
    +    FontMetricAscentE,
    +    FontMetricLineE,
    +    FontMetricSizeE
    +} FontMetricE;
    +
    +static char *sLicence;      // FontLicence's last text (kept until the next call)
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +int32_t         FontAddData(const char *name, const uint8_t *data, int32_t len);
    +int32_t         FontAddFile(const char *path);
    +int32_t         FontAddResources(const char *path);
    +int32_t         FontAscent(const char *family, int32_t size, int32_t style);
    +int32_t         FontFamilyCount(void);
    +int32_t         FontFamilyFixed(const char *family);
    +const char     *FontFamilyName(int32_t index);
    +const char     *FontLicence(const char *family, int32_t size, int32_t style);
    +int32_t         FontLineHeight(const char *family, int32_t size, int32_t style);
    +int32_t         FontNearestSize(const char *family, int32_t size, int32_t style);
    +void            FontRescan(void);
    +int32_t         FontSizeAt(const char *family, int32_t style, int32_t index);
    +int32_t         FontSizeCount(const char *family, int32_t style);
    +int32_t         FontTextWidth(const char *family, int32_t size, int32_t style, const char *text);
    +static int32_t  metric(const char *family, int32_t size, int32_t style, FontMetricE which);
    +static int32_t  runningApp(void);
    +
    +
    +// Add the .DFN in bytes for the running program; True when added.
    +int32_t FontAddData(const char *name, const uint8_t *data, int32_t len) {
    +    return (len > 0 && dvxFontAddMemory(runningApp(), data, (uint32_t)len, name)) ? BAS_TRUE : BAS_FALSE;
    +}
    +
    +
    +// Add the .DFN file at path for the running program; True when added.
    +int32_t FontAddFile(const char *path) {
    +    return dvxFontAddFile(runningApp(), path) ? BAS_TRUE : BAS_FALSE;
    +}
    +
    +
    +// Add the font resources of the file at path for the running program;
    +// how many were added.
    +int32_t FontAddResources(const char *path) {
    +    return dvxFontAddResources(runningApp(), path);
    +}
    +
    +
    +// Rows from the top of a line to the baseline of the font.
    +int32_t FontAscent(const char *family, int32_t size, int32_t style) {
    +    return metric(family, size, style, FontMetricAscentE);
    +}
    +
    +
    +int32_t FontFamilyCount(void) {
    +    return dvxFontFamilyCount();
    +}
    +
    +
    +int32_t FontFamilyFixed(const char *family) {
    +    return dvxFontFamilyFixed(family) ? BAS_TRUE : BAS_FALSE;
    +}
    +
    +
    +// Family index ("" out of range).
    +const char *FontFamilyName(int32_t index) {
    +    const char *name = dvxFontFamilyName(index);
    +
    +    return name ? name : "";
    +}
    +
    +
    +// The notice and licence texts of the font ("" for the system font).
    +const char *FontLicence(const char *family, int32_t size, int32_t style) {
    +    BitmapFontT *font = dvxFontOpen(family, size, (uint32_t)style);
    +
    +    free(sLicence);
    +    sLicence = dvxFontLicence(font);
    +    dvxFontClose(font);
    +    return sLicence ? sLicence : "";
    +}
    +
    +
    +// The height of a line of the font.
    +int32_t FontLineHeight(const char *family, int32_t size, int32_t style) {
    +    return metric(family, size, style, FontMetricLineE);
    +}
    +
    +
    +// The size the font is drawn at when size is asked for (the nearest the
    +// family has).
    +int32_t FontNearestSize(const char *family, int32_t size, int32_t style) {
    +    return metric(family, size, style, FontMetricSizeE);
    +}
    +
    +
    +void FontRescan(void) {
    +    dvxFontRescan();
    +}
    +
    +
    +// Size index (0 .. FontSizeCount - 1) of family in style; 0 out of range.
    +int32_t FontSizeAt(const char *family, int32_t style, int32_t index) {
    +    int32_t sizes[DFN_STRIKE_MAX];
    +    int32_t count = dvxFontSizes(family, (uint32_t)style, sizes, DFN_STRIKE_MAX);
    +
    +    return (index >= 0 && index < count && index < DFN_STRIKE_MAX) ? sizes[index] : 0;
    +}
    +
    +
    +int32_t FontSizeCount(const char *family, int32_t style) {
    +    return dvxFontSizes(family, (uint32_t)style, NULL, 0);
    +}
    +
    +
    +// The width of text drawn in the font.
    +int32_t FontTextWidth(const char *family, int32_t size, int32_t style, const char *text) {
    +    BitmapFontT *font = dvxFontOpen(family, size, (uint32_t)style);
    +
    +    if (!font) {
    +        return 0;
    +    }
    +
    +    int32_t width = textWidth(font, text);
    +
    +    dvxFontClose(font);
    +    return width;
    +}
    +
    +
    +// One measure of the font family at size in style (0 out of memory)
    +static int32_t metric(const char *family, int32_t size, int32_t style, FontMetricE which) {
    +    BitmapFontT *font = dvxFontOpen(family, size, (uint32_t)style);
    +
    +    if (!font) {
    +        return 0;
    +    }
    +
    +    int32_t value = (which == FontMetricAscentE) ? font->ascent : (which == FontMetricLineE) ? font->charHeight : font->pixelSize;
    +
    +    dvxFontClose(font);
    +    return value;
    +}
    +
    +
    +// The app id of the program whose code is running (0: none, the shell)
    +static int32_t runningApp(void) {
    +    return dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
    +}
    diff --git a/src/libs/kpunch/libdvx/dvxTypes.h b/src/libs/kpunch/libdvx/dvxTypes.h
    index 63ef0361..7cdf5b09 100644
    --- a/src/libs/kpunch/libdvx/dvxTypes.h
    +++ b/src/libs/kpunch/libdvx/dvxTypes.h
    @@ -65,7 +65,7 @@
     // src/test/abi/abiSizes.txt (make -C src/test/abi update) in the same
     // change; the abi test fails when a public struct changes size without a
     // bump, and when the table was not regenerated after one.
    -#define DVX_ABI_VERSION 8
    +#define DVX_ABI_VERSION 9
     
     // A module's stamp is named , e.g. the
     // stamp of LIBS/KPUNCH/LIBDVX/LIBDVX.LIB is libdvxAbiVersion.
    @@ -222,29 +222,119 @@ typedef struct {
     // Bitmap font
     // ============================================================
     //
    -// Fixed-width 8-pixel-wide bitmap fonts only. This simplifies text
    -// rendering enormously: character positions are pure multiplication
    -// (x = col * 8), glyph lookup is a single array index, and the 1bpp
    -// format means each scanline of a glyph is exactly one byte.
    -//
    -// One font size is provided (8x16), matching the standard
    -// VGA ROM font and CP437 encoding. Proportional fonts would require
    -// glyph-width tables, kerning, and per-character positioning  -- none of
    -// which is worth the complexity for a DOS-era window manager targeting
    -// 640x480 screens. The 8-pixel width also aligns nicely with byte
    -// boundaries, enabling per-scanline glyph rendering without bit shifting.
    +// One type for every font DVX draws with:
    +//   - the built-in system font (dvxFont8x16): 8-pixel cells of the
    +//     standard VGA 8x16 font, code page 437, one byte per glyph row in
    +//     glyphData -- glyphs is NULL, and text takes the byte-aligned fast
    +//     paths in dvxDraw.c;
    +//   - a loaded font (dvxFontOpen, dvxText.h): a strike of a .DFN file
    +//     (dvxFontFmt.h) or the system font made bold or italic.  glyphs
    +//     holds each character's advance and bitmap box, glyphAt where its
    +//     bitmap starts in glyphData; rows are (width + 7) / 8 bytes, the
    +//     leftmost pixel in the top bit.  Every character has a glyph: the
    +//     loader fills in what a face lacks.
    +// charHeight is the line height and charWidth the cell width of a
    +// fixed-pitch font; for a proportional one (FONT_FLAG_PROPORTIONAL)
    +// charWidth is the width of the digit 0, for sizing a field "so many
    +// characters wide".  Text is measured with textWidth / textWidthN.
     
    -#define FONT_CHAR_WIDTH  8    // fixed glyph width in pixels
    -#define FONT_CHAR_HEIGHT 16   // fixed glyph height in pixels
    +#define FONT_CHAR_WIDTH  8    // the system font's glyph width in pixels
    +#define FONT_CHAR_HEIGHT 16   // the system font's glyph height in pixels
    +#define FONT_FAMILY_MAX  32   // a font family's name with its NUL
    +
    +// BitmapFontT.style and the style arguments of dvxFontOpen and
    +// drawTextStyled.  Bold and italic are faces; underline and strike-
    +// through are lines every text call draws with a font that has them
    +// (as in VB, where they are part of the font).
    +#define FONT_STYLE_BOLD       0x0001
    +#define FONT_STYLE_ITALIC     0x0002
    +#define FONT_STYLE_UNDERLINE  0x0004
    +#define FONT_STYLE_STRIKE     0x0008
    +#define FONT_STYLE_FACE       (FONT_STYLE_BOLD | FONT_STYLE_ITALIC)
    +#define FONT_STYLE_LINES      (FONT_STYLE_UNDERLINE | FONT_STYLE_STRIKE)
    +
    +// BitmapFontT.flags
    +#define FONT_FLAG_PROPORTIONAL 0x0001   // characters differ in width
    +#define FONT_FLAG_SYNTH_BOLD   0x0002   // bold made from the regular face
    +#define FONT_FLAG_SYNTH_ITALIC 0x0004   // italic made by slanting the upright face
    +#define FONT_FLAG_GRAY         0x0008   // anti-aliased: glyph pixels of FONT_GRAY_BPP bits (FONT_BPP)
    +
    +// One character of a loaded font.  The same record is stored in .DFN
    +// files (dvxFontFmt.h).
    +typedef struct {
    +    uint8_t advance;        // pen movement after the glyph
    +    uint8_t width;          // bitmap columns
    +    uint8_t height;         // bitmap rows
    +    int8_t  left;           // pen position to the bitmap's left column
    +    int8_t  top;            // baseline to the bitmap's top row (up is positive)
    +    uint8_t flags;          // FONT_GLYPH_*
    +} FontGlyphT;
    +
    +_Static_assert(sizeof(FontGlyphT) == 6, "FontGlyphT is read whole from .DFN files: no padding");
    +
    +// FontGlyphT.flags
    +#define FONT_GLYPH_MISSING 0x01     // the face has no glyph for the character (in a file; never in a loaded font)
    +
    +// Bytes in one row of a glyph bitmap of width columns, the byte and bit
    +// of column x in a row (the leftmost pixel in the top bit), and a
    +// glyph's bitmap size -- for 1 bit per pixel
    +#define FONT_ROW_BYTES(width) FONT_ROW_BYTES_BPP(width, 1)
    +#define FONT_PIXEL_BYTE(x)    ((x) / 8)
    +#define FONT_PIXEL_BIT(x)     (0x80 >> ((x) % 8))
    +#define FONT_GLYPH_BYTES(g)   FONT_GLYPH_BYTES_BPP(g, 1)
    +
    +// A gray (anti-aliased) glyph: 2 bits per pixel, levels 0 (none) to
    +// FONT_GRAY_FULL, the leftmost pixel in the top bits of a byte
    +#define FONT_GRAY_BPP                 2
    +#define FONT_GRAY_FULL                3
    +#define FONT_GRAY_HALF                2         // the level that counts as ink without blending (1-bit glyphs, 8 bpp)
    +#define FONT_GRAY_PER_BYTE            4
    +#define FONT_GRAY_SHIFT(x)            (6 - 2 * ((x) % FONT_GRAY_PER_BYTE))
    +#define FONT_GRAY_LEVEL(row, x)       (((row)[(x) / FONT_GRAY_PER_BYTE] >> FONT_GRAY_SHIFT(x)) & FONT_GRAY_FULL)
    +#define FONT_ROW_BYTES_BPP(width, bpp) (((width) * (bpp) + 7) / 8)
    +#define FONT_GLYPH_BYTES_BPP(g, bpp)   ((uint32_t)FONT_ROW_BYTES_BPP((g)->width, bpp) * (g)->height)
     
     typedef struct {
    -    int32_t        charWidth;    // fixed width per glyph (e.g. 8)
    -    int32_t        charHeight;   // 16 (only 8x16 font provided)
    -    int32_t        firstChar;    // ASCII code of first glyph (typically 0)
    -    int32_t        numChars;     // number of glyphs (typically 256)
    -    const uint8_t *glyphData;    // packed 1bpp, charHeight bytes per glyph
    +    int32_t           charWidth;    // cell width (fixed pitch) or the digit 0's advance
    +    int32_t           charHeight;   // line height
    +    int32_t           firstChar;    // code of the first glyph (0)
    +    int32_t           numChars;     // number of glyphs (256)
    +    const uint8_t    *glyphData;    // system font: charHeight bytes per glyph; loaded: the bitmaps
    +    const FontGlyphT *glyphs;       // loaded font: numChars glyphs; NULL: the system font's cells
    +    const uint32_t   *glyphAt;      // loaded font: each glyph's bitmap offset in glyphData
    +    int32_t           ascent;       // rows from the top of a line to the baseline
    +    int32_t           pixelSize;    // the size the font was made at
    +    uint32_t          style;        // FONT_STYLE_*: the face's bold / italic, the lines drawn with text
    +    uint32_t          flags;        // FONT_FLAG_*
    +    int32_t           underlineY;   // underline (and accelerator mark) row from the top of a line
    +    int32_t           strikeY;      // strike-through row from the top of a line
    +    int32_t           lineThick;    // rows in an underline or strike-through line
    +    void             *priv;         // the font cache's entry (dvxText.c); NULL: not from the cache
    +    uint32_t          serial;       // a loaded font's own number, never reused, new when its glyphs are built again (0: the system font as compiled in)
     } BitmapFontT;
     
    +// The bits per pixel of a font's glyph bitmaps
    +#define FONT_BPP(font) (((font)->flags & FONT_FLAG_GRAY) ? FONT_GRAY_BPP : 1)
    +
    +// The font a cache of text measurements was made with, as it was then: a
    +// font's glyphs can be built again under the same pointer (smoothing
    +// turned on or off), and its serial says so.  Zeroed: nothing measured.
    +typedef struct {
    +    const BitmapFontT *font;
    +    uint32_t           serial;
    +} FontStampT;
    +
    +// Whether stamp is font as it is now
    +static inline bool fontStampIs(const FontStampT *stamp, const BitmapFontT *font) {
    +    return stamp->font == font && font && stamp->serial == font->serial;
    +}
    +
    +
    +static inline void fontStampSet(FontStampT *stamp, const BitmapFontT *font) {
    +    stamp->font   = font;
    +    stamp->serial = font ? font->serial : 0;
    +}
    +
     // ============================================================
     // Color scheme
     // ============================================================
    diff --git a/src/libs/kpunch/libdvx/dvxWgt.h b/src/libs/kpunch/libdvx/dvxWgt.h
    index 933b3359..1e04c595 100644
    --- a/src/libs/kpunch/libdvx/dvxWgt.h
    +++ b/src/libs/kpunch/libdvx/dvxWgt.h
    @@ -175,6 +175,7 @@ typedef enum {
     #define WCLASS_ACCEL_WHEN_HIDDEN   0x00002000  // accel matching works even when invisible
     #define WCLASS_HAS_TEXT            0x00004000  // first field of w->data is (const char *), owned by widget (strdup/free)
     #define WCLASS_RAISES_EVENTS       0x00008000  // the class fires the universal mouse, click, key and scroll callbacks itself (wgtFireEvent), the dispatcher does not (a BASIC UserControl: its host gets what its code raises)
    +#define WCLASS_FIXED_FONT          0x00010000  // text edited by columns: a proportional font draws as the system font (wgtFontFor)
     
     // Method IDs -- stable ABI, never reorder, never reuse.
     // New methods are appended at the end with the next sequential ID.
    @@ -343,6 +344,10 @@ typedef struct WidgetT {
         uint32_t         fgColor;
         uint32_t         bgColor;
     
    +    // Font (wgtSetFont; NULL = the container's, see wgtFontFor).  Held
    +    // open while it is set and closed when the widget is destroyed.
    +    BitmapFontT     *font;
    +
         // State
         bool             visible;
         bool             enabled;
    @@ -401,12 +406,21 @@ typedef struct WidgetT {
         void           (*onEvent)(struct WidgetT *w, const char *name, WgtValueT *args, int32_t argc);
     } WidgetT;
     
    +// The font w draws with: its own (wgtSetFont), else the nearest
    +// container's up the parent chain, else inherited -- the font its caller
    +// passes down (the application's; NULL for a metrics query that needs
    +// none, which then gets NULL when no font is set).  A class that edits text by columns
    +// (WCLASS_FIXED_FONT) given a proportional font gets the system font.
    +// The class dispatch helpers below hand every method this font, so a
    +// container's font reaches the widgets inside it.
    +const BitmapFontT *wgtFontFor(const WidgetT *w, const BitmapFontT *inherited);
    +
     static inline void wclsCalcMinSize(WidgetT *w, const BitmapFontT *font) {
         typedef void (*FnT)(WidgetT *, const BitmapFontT *);
         FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_CALC_MIN_SIZE] : NULL;
     
         if (fn) {
    -        fn(w, font);
    +        fn(w, wgtFontFor(w, font));
         }
     }
     
    @@ -468,7 +482,7 @@ static inline void wclsGetLayoutMetrics(const WidgetT *w, const BitmapFontT *fon
         FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_GET_LAYOUT_METRICS] : NULL;
     
         if (fn) {
    -        fn(w, font, pad, gap, extraTop, borderW);
    +        fn(w, wgtFontFor(w, font), pad, gap, extraTop, borderW);
         }
     }
     
    @@ -478,7 +492,7 @@ static inline void wclsGetPopupRect(const WidgetT *w, const BitmapFontT *font, i
         FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_GET_POPUP_RECT] : NULL;
     
         if (fn) {
    -        fn(w, font, contentH, popX, popY, popW, popH);
    +        fn(w, wgtFontFor(w, font), contentH, popX, popY, popW, popH);
         }
     }
     
    @@ -510,7 +524,7 @@ static inline void wclsLayout(WidgetT *w, const BitmapFontT *font) {
         FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_LAYOUT] : NULL;
     
         if (fn) {
    -        fn(w, font);
    +        fn(w, wgtFontFor(w, font));
         }
     }
     
    @@ -643,7 +657,7 @@ static inline void wclsPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const
         FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_PAINT] : NULL;
     
         if (fn) {
    -        fn(w, d, ops, font, colors);
    +        fn(w, d, ops, wgtFontFor(w, font), colors);
         }
     }
     
    @@ -653,7 +667,7 @@ static inline void wclsPaintOverlay(WidgetT *w, DisplayT *d, const BlitOpsT *ops
         FnT fn = w->wclass ? (FnT)w->wclass->handlers[WGT_METHOD_PAINT_OVERLAY] : NULL;
     
         if (fn) {
    -        fn(w, d, ops, font, colors);
    +        fn(w, d, ops, wgtFontFor(w, font), colors);
         }
     }
     
    @@ -754,6 +768,16 @@ void wgtSetText(WidgetT *w, const char *text);
     // Enable/disable a widget
     void wgtSetEnabled(WidgetT *w, bool enabled);
     
    +// The font w draws with now (wgtFontFor with the application's font):
    +// for event handlers, which are given none.
    +const BitmapFontT *wgtFont(const WidgetT *w);
    +
    +// Give w the font of family at the size nearest size in style
    +// (dvxFontOpen, dvxText.h); the widgets inside it draw with it too unless
    +// they have their own.  NULL family: w uses its container's again.
    +// Relays out the window.  False when out of memory (w keeps its font).
    +bool wgtSetFont(WidgetT *w, const char *family, int32_t size, uint32_t style);
    +
     // Set read-only mode (allows scrolling/selection but blocks editing)
     void wgtSetReadOnly(WidgetT *w, bool readOnly);
     
    diff --git a/src/libs/kpunch/libdvx/dvxWgtP.h b/src/libs/kpunch/libdvx/dvxWgtP.h
    index c4aa443a..152b6af9 100644
    --- a/src/libs/kpunch/libdvx/dvxWgtP.h
    +++ b/src/libs/kpunch/libdvx/dvxWgtP.h
    @@ -153,6 +153,10 @@ void widgetRemoveChild(WidgetT *parent, WidgetT *child);
     // from the poll list.  Call for every destroyed node.
     void widgetClearReferences(WidgetT *w);
     
    +// Frees a widget struct and closes the font it holds (its children and
    +// data are its destroy path's).  The one place a widget is freed.
    +void widgetFree(WidgetT *w);
    +
     // Closes the open popup (if any): dirties its overlay rect and fully
     // invalidates its window so the overlay pixels are erased.  The one close
     // path for click-outside, cross-window clicks, Escape and focus changes.
    @@ -277,6 +281,13 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value);
     void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
     void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
     
    +// A display onto w's window content buffer, clipped to w and its
    +// ancestors, for a quick repaint (WGT_METHOD_QUICK_REPAINT) outside the
    +// paint walk.  False when w cannot be painted that way now (hidden,
    +// clipped away, or in a scroll container -- then it is invalidated for
    +// the normal paint).
    +bool widgetQuickPaintBegin(WidgetT *w, DisplayT *cd);
    +
     // ============================================================
     // Pressable helpers (widgetOps.c)
     // ============================================================
    diff --git a/src/libs/kpunch/libdvx/games.dhs b/src/libs/kpunch/libdvx/games.dhs
    index d066c1cb..2adcf988 100644
    --- a/src/libs/kpunch/libdvx/games.dhs
    +++ b/src/libs/kpunch/libdvx/games.dhs
    @@ -34,7 +34,7 @@
     
     .h1 Writing Games for DVX
     
    -A game in DVX is a program with its own main loop (hasMainLoop = true in its SHELL_APP_DESCRIPTOR). DVX gives it its own task: appMain runs the game loop itself, draws into the game's window, and waits for each frame on the high-resolution clock while the other programs keep running. Nothing in DVX has to stop for a game: the desktop, other programs, the Task Manager (Ctrl+Esc) and help all stay available.
    +A game in DVX is a program with its own main loop (hasMainLoop = true in its SHELL_APP_DESCRIPTOR). DVX gives it its own task: appMain runs the game loop itself, draws into the game's window, and waits for each frame on the high-resolution clock while the other programs keep running. Nothing in DVX has to stop for a game: the desktop, other programs, the Task Manager (Ctrl+Esc), and help all stay available.
     
     .h2 Starting From a Template
     
    @@ -43,7 +43,7 @@ A game in DVX is a program with its own main loop (hasMainLoop = true in its SHE
       bin/sdk/newProject.sh basicgame myGame   a DVX BASIC game
     .endcode
     
    -The C template is a small but complete game, and every part of this guide is shown in it: a game input window, a 320x200 screen of 8-bit colour numbers drawn scaled into the window on any desktop colour depth, frame pacing, the arrow keys and a joystick, a sound effect and music carried inside the .app as resources, tones for the end of a round, the best score saved in the program's settings folder, its own help file (F1) and icon. The BASIC template does the same in a few dozen lines with FRAMEWAIT, GameKeyDown, STICK, STRIG and PLAY.
    +The C template is a small but complete game, and every part of this guide is shown in it: a game input window, a 320x200 screen of 8-bit colour numbers drawn scaled into the window on any desktop colour depth, frame pacing, the arrow keys and a joystick, a sound effect and music carried inside the .app as resources, tones for the end of a round, the best score saved in the program's settings folder, its own help file (F1) and icon. The BASIC template does the same in a few dozen lines with FRAMEWAIT, GameKeyDown, STICK, STRIG, and PLAY.
     
     .h2 The Shape of a Game
     
    @@ -72,18 +72,18 @@ int32_t appMain(DxeAppContextT *ctx) {
     
     The sections that follow cover each part.
     
    -.link games.input Keyboard, Mouse and Joysticks
    +.link games.input Keyboard, Mouse, and Joysticks
     .link games.timing Timing and Frame Pacing
     .link games.screen The Screen: Framebuffers and Palettes
    -.link games.sound Sound, Music and MIDI
    +.link games.sound Sound, Music, and MIDI
     .link games.files Data Files and Saved Games
     .link games.packaging Packaging and Ending
     .link games.basic Games in DVX BASIC
     .link games.porting Porting a Game Library to DVX
     
     .topic games.input
    -.title Keyboard, Mouse and Joysticks
    -.toc 1 Keyboard, Mouse and Joysticks
    +.title Keyboard, Mouse, and Joysticks
    +.toc 1 Keyboard, Mouse, and Joysticks
     .index Game Input
     .index gameInput
     .index onRawKey
    @@ -91,7 +91,7 @@ The sections that follow cover each part.
     .index Held keys
     .index Reserved keys
     
    -.h1 Keyboard, Mouse and Joysticks
    +.h1 Keyboard, Mouse, and Joysticks
     
     .h2 Game Input Windows
     
    @@ -103,7 +103,7 @@ win->onKey     = gameKey;       // typed keys: every key but three
     win->onRawKey  = gameRawKey;    // each key going down and up
     .endcode
     
    -While the window has the focus, DVX keeps only three keys, so the user can always switch away, open the Task Manager or close the game:
    +While the window has the focus, DVX keeps only three keys, so the user can always switch away, open the Task Manager, or close the game:
     
     .table
       Key         Always does
    @@ -113,9 +113,9 @@ While the window has the focus, DVX keeps only three keys, so the user can alway
       Alt+F4      Close the window (its onClose)
     .endtable
     
    -Every other key, F1, F10, Alt+letter and the accelerators included, goes to onKey instead of opening help or menus. The game decides what F1 does (the template opens its help with shellOpenHelp); its menus still work with the mouse. onRawKey gets a RawKeyEventT for every key going down or up: the set-1 scancode with an extended flag for the grey keys, down or up, whether it is a typematic repeat, and the modifiers. Shift, Ctrl and Alt are keys of their own there, left and right apart.
    +Every other key, F1, F10, Alt+letter, and the accelerators included, goes to onKey instead of opening help or menus. The game decides what F1 does (the template opens its help with shellOpenHelp); its menus still work with the mouse. onRawKey gets a RawKeyEventT for every key going down or up: the set-1 scancode with an extended flag for the grey keys, down or up, whether it is a typematic repeat, and the modifiers. Shift, Ctrl, and Alt are keys of their own there, left and right apart.
     
    -The key that completes one of the three (Esc with Ctrl held, Tab or F4 with Alt held) never reaches the game: not in onRawKey, not in the held-key table, not its repeats and not its release. The modifier itself does, because when Ctrl or Alt goes down nobody can tell yet what will follow. A game that treats any key as "go on" therefore still sees the Ctrl or Alt of Ctrl+Esc or Alt+Tab.
    +The key that completes one of the three (Esc with Ctrl held, Tab or F4 with Alt held) never reaches the game: not in onRawKey, not in the held-key table, not its repeats, and not its release. The modifier itself does, because when Ctrl or Alt goes down nobody can tell yet what will follow. A game that treats any key as "go on" therefore still sees the Ctrl or Alt of Ctrl+Esc or Alt+Tab.
     
     .h2 Held Keys
     
    @@ -163,7 +163,7 @@ For work that must happen at a steady rate however busy the programs are (a musi
     
     .h1 The Screen: Framebuffers and Palettes
     
    -A window's content buffer (win->contentBuf, win->contentPitch bytes per row, contentW x contentH pixels) is in the desktop's own pixel format: 8, 15, 16 or 32 bits per pixel, as dvxGetDisplay(ctx)->format says. A game that must look the same everywhere draws into a framebuffer of its own (for example 320x200 bytes, each a colour number into a 256-colour game palette) and converts only what changed into the content buffer each frame.
    +A window's content buffer (win->contentBuf, win->contentPitch bytes per row, contentW x contentH pixels) is in the desktop's own pixel format: 8, 15, 16, or 32 bits per pixel, as dvxGetDisplay(ctx)->format says. A game that must look the same everywhere draws into a framebuffer of its own (for example 320x200 bytes, each a colour number into a 256-colour game palette) and converts only what changed into the content buffer each frame.
     
     .h2 The Colour Lookup Table
     
    @@ -189,7 +189,7 @@ for (int32_t i = 0; i < 256; i++) {
       8-bit       1 byte        The nearest of the desktop's 256 colours.
     .endtable
     
    -On an 8-bit desktop all windows share the one desktop palette: a 6x6x6 colour cube, a 16-step grey ramp and the chrome colours (see dvxPal.h). A game cannot change it; gamePalNearest finds the closest entry, and a GamePalCacheT remembers the answer per 15-bit colour when there are many colours to map (gamePalCacheSync starts over if the desktop palette changed). Gradients come out banded; design for it, or ask players to use a 15-bit or better desktop for the best look.
    +On an 8-bit desktop all windows share the one desktop palette: a 6x6x6 colour cube, a 16-step grey ramp, and the chrome colours (see dvxPal.h). A game cannot change it; gamePalNearest finds the closest entry, and a GamePalCacheT remembers the answer per 15-bit colour when there are many colours to map (gamePalCacheSync starts over if the desktop palette changed). Gradients come out banded; design for it, or ask players to use a 15-bit or better desktop for the best look.
     
     Palette animation (colour cycling, fades) works at every depth: change the game palette entries, look up just those entries again, and redraw the pixels that use them. Games whose hardware picks a palette per scan line keep one small table per palette and pick the table per row.
     
    @@ -236,21 +236,21 @@ if (!dvxExclusivePresent8(ctx, fb, 320, 200, 320)) {
     .list
     .item dvxExclusiveBegin sets the smallest mode of at least the size asked for, at the depth asked for when there is one (0: any). At 8 bits per pixel a request that fits 320x200 gets VGA mode 13h, and one that fits 320x240 gets VGA Mode X (square pixels, and two pages: a whole-screen flush is shown from the next retrace, so frames never tear), unless the card has a VBE mode no larger. Either way the game draws into an ordinary linear backbuffer. The display may be larger than asked for; dvxExclusiveDisplay says what it is.
     .item The game's window must have the focus. When it loses it (Alt+Tab, Ctrl+Esc, a dialog, another program's window coming to the front) the desktop comes back and the callback hears ExclusiveSuspendedE; when the window has the focus again the mode is set again and it hears ExclusiveActiveE. Draw the whole frame then: the screen starts black.
    -.item While the game has the screen, every key but Alt+Tab, Ctrl+Esc and Alt+F4 goes to its window, as in a game input window; the mouse cursor is not drawn; onMouse gets whole-screen coordinates.
    +.item While the game has the screen, every key but Alt+Tab, Ctrl+Esc, and Alt+F4 goes to its window, as in a game input window; the mouse cursor is not drawn; onMouse gets whole-screen coordinates.
     .item dvxExclusivePresent8 shows a picture of palette numbers on the whole screen, scaled by the largest whole factor that fits and centred on black, in any pixel format. At 8 bits per pixel the palette is the screen's own: dvxExclusiveSetPalette changes colours at once, so palette animation costs nothing, and a hardware model of up to 16 palettes of 16 colours fits in the 256 entries.
     .item To draw yourself, draw into dvxExclusiveDisplay(ctx)->backBuf (DVX's draw functions work on it with dvxExclusiveBlitOps) and call dvxExclusiveFlush. dvxExclusiveWaitRetrace waits for the vertical retrace before a flush, so the frame is not torn.
    -.item dvxExclusiveEnd gives the screen back. Closing the window, the program ending and End Task do too.
    +.item dvxExclusiveEnd gives the screen back. Closing the window, the program ending, and End Task do too.
     .endlist
     
     .topic games.sound
    -.title Sound, Music and MIDI
    -.toc 1 Sound, Music and MIDI
    +.title Sound, Music, and MIDI
    +.toc 1 Sound, Music, and MIDI
     .index Sound
     .index Music
     .index MIDI
     .index audioPump
     
    -.h1 Sound, Music and MIDI
    +.h1 Sound, Music, and MIDI
     
     Games play sound on whatever the computer has: a Sound Blaster family card, an FM synthesizer chip, a MIDI port, the PC speaker, or nothing. Nothing fails because of missing hardware: audioOpen returns NULL (or the device plays tones only), and the game carries on silently. Several programs can play at once, and everything a program opened is stopped and released when it ends.
     
    @@ -259,15 +259,15 @@ Games play sound on whatever the computer has: a Sound Blaster family card, an F
     .list
     .item audioOpen() gives the program an audio context (the first one opened starts the sound device).
     .item audioSampleLoad / audioSampleLoadMem load a WAV or VOC file (from memory: a resource, say); audioSampleCreate makes a sample from raw PCM (8-bit unsigned or 16-bit signed, mono or stereo, any rate from 4000 to 48000 Hz).
    -.item audioPlay(sample, volume, pan, loop) plays it on a voice; the voice id sets volume, pan and pitch while it plays (audioVoiceSet...), audioVoiceStop stops it, and audioSetEndFn tells when it ends.
    -.item audioTone plays a square, triangle or noise tone of a frequency and length; audioNotesOpen gives a queue of notes and rests played back to back, each to the exact sample (jingles, chip-style tunes).
    +.item audioPlay(sample, volume, pan, loop) plays it on a voice; the voice id sets volume, pan, and pitch while it plays (audioVoiceSet...), audioVoiceStop stops it, and audioSetEndFn tells when it ends.
    +.item audioTone plays a square, triangle, or noise tone of a frequency and length; audioNotesOpen gives a queue of notes and rests played back to back, each to the exact sample (jingles, chip-style tunes).
     .item audioStreamOpen takes the game's own sound at any rate: written with audioStreamWrite, or asked for by a fill function.
     .item audioSetVolume sets the program's volume; the master volume belongs to the user (Control Panel).
     .endlist
     
     .h2 Music (music.h)
     
    -musicLoad or musicLoadMem load a tracker module (MOD, S3M, XM, IT) or a Standard MIDI File; musicPlay(music, loop) plays it, with pause, resume, stop, volume, position and an end notification. MIDI plays on the best synthesizer there is: an external one on the MPU-401 port when the user chose it, else the FM chip with a General MIDI bank, else DVX's own FM software synthesizer; modules play on any card that plays sampled sound. musicMidiOpen gives live MIDI (note on and off, program change, controllers, pitch bend), and musicMmlPlay plays tunes written in QBasic's music macro language.
    +musicLoad or musicLoadMem load a tracker module (MOD, S3M, XM, IT) or a Standard MIDI File; musicPlay(music, loop) plays it, with pause, resume, stop, volume, position, and an end notification. MIDI plays on the best synthesizer there is: an external one on the MPU-401 port when the user chose it, else the FM chip with a General MIDI bank, else DVX's own FM software synthesizer; modules play on any card that plays sampled sound. musicMidiOpen gives live MIDI (note on and off, program change, controllers, pitch bend), and musicMmlPlay plays tunes written in QBasic's music macro language.
     
     .h2 The FM Chip and the MIDI Port
     
    @@ -275,9 +275,9 @@ For music drivers of its own, a game can allocate two-operator FM voices shared
     
     .h2 Keeping Sound Fed
     
    -Sampled sound is mixed from the sound card's own interrupt, so it keeps playing however busy the game is. End notifications, pull streams, modules and the software synthesizer are made in the desktop's idle time; a game that redraws every frame should call audioPump() and musicPump() once a frame so they keep up. Modules use floating point: on a computer without a maths coprocessor they are slow.
    +Sampled sound is mixed from the sound card's own interrupt, so it keeps playing however busy the game is. End notifications, pull streams, modules, and the software synthesizer are made in the desktop's idle time; a game that redraws every frame should call audioPump() and musicPump() once a frame so they keep up. Modules use floating point: on a computer without a maths coprocessor they are slow.
     
    -See the Sound Library, the Music Library and the Joystick Library for every function.
    +See the Sound Library, the Music Library, and the Joystick Library for every function.
     
     .topic games.files
     .title Data Files and Saved Games
    @@ -323,11 +323,11 @@ configDir is CONFIG plus the folder the .app is in: a game in APPS\ACME\MYGAME k
     
     .h2 Packaging
     
    -make package lays out pkg/APPS/// with NAME.app, NAME.hlp and the files listed in DATA; zip the contents of pkg/ and the player unpacks it into the DVX directory (make install DVX_DIR=... does it for you). The .app carries its icon, name, author and description for Program Manager (NAME.res). Every game should have its own help (NAME.dhs, compiled to NAME.hlp), since F1 in a game input window goes to the game: open the help from the game's F1 and from Help > Contents. A game built on a static library of its own links it in with the LIBS variable (LIBS = port/libgame.a).
    +make package lays out pkg/APPS/// with NAME.app, NAME.hlp, and the files listed in DATA; zip the contents of pkg/ and the player unpacks it into the DVX directory (make install DVX_DIR=... does it for you). The .app carries its icon, name, author, and description for Program Manager (NAME.res). Every game should have its own help (NAME.dhs, compiled to NAME.hlp), since F1 in a game input window goes to the game: open the help from the game's F1 and from Help > Contents. A game built on a static library of its own links it in with the LIBS variable (LIBS = port/libgame.a).
     
     .h2 Ending
     
    -Close the game's window and return from appMain. Every sound, music, joystick handle and timer the program opened is released when it ends, however it ends, so a crash or End Task leaves nothing playing. A game (or any program) that calls exit() or abort() ends only itself, like End Task: its windows close, its timers are removed and DVX keeps running. Handlers registered with atexit are not run; clean up before calling exit(), or return from appMain. malloc and free work as usual; DVX counts each program's memory (the Task Manager shows it).
    +Close the game's window and return from appMain. Every sound, music, joystick handle, and timer the program opened is released when it ends, however it ends, so a crash or End Task leaves nothing playing. A game (or any program) that calls exit() or abort() ends only itself, like End Task: its windows close, its timers are removed, and DVX keeps running. Handlers registered with atexit are not run; clean up before calling exit(), or return from appMain. malloc and free work as usual; DVX counts each program's memory (the Task Manager shows it).
     
     .topic games.basic
     .title Games in DVX BASIC
    @@ -353,7 +353,7 @@ Sub tmrStart_Timer ()
     End Sub
     .endcode
     
    -GameKeyDown (game.bas) tells whether a key is held, GameKeyClear forgets the held keys, FrameMissed() counts the frames FRAMEWAIT skipped and GameWaitRetrace waits for the screen's retrace; QBasic's STICK and STRIG read joysticks (the keyboard joystick when there is none); BEEP, SOUND and PLAY make sound, PLAY "MB ..." in the background so the game does not wait. sound.bas plays WAV files and tones, sounds loaded once and played with volume, pan and pitch, sound the game makes as it goes (streams) and FM instruments; music.bas plays modules and MIDI files, and joystick.bas reads joysticks in full. See Sound, Joysticks and Frames and Include Libraries in the BASIC reference.
    +GameKeyDown (game.bas) tells whether a key is held, GameKeyClear forgets the held keys, FrameMissed() counts the frames FRAMEWAIT skipped, and GameWaitRetrace waits for the screen's retrace; QBasic's STICK and STRIG read joysticks (the keyboard joystick when there is none); BEEP, SOUND, and PLAY make sound, PLAY "MB ..." in the background so the game does not wait. sound.bas plays WAV files and tones, sounds loaded once and played with volume, pan, and pitch, sound the game makes as it goes (streams), and FM instruments; music.bas plays modules and MIDI files, and joystick.bas reads joysticks in full. See Sound, Joysticks, and Frames and Include Libraries in the BASIC reference.
     
     .topic games.porting
     .title Porting a Game Library to DVX
    @@ -368,7 +368,7 @@ Portable game libraries for DOS-era machines usually keep everything machine-spe
     
     .h2 The Model: a Window, a Task, a Virtual Frame Rate
     
    -The game becomes a DVX program with its own main loop: appMain calls the game's main() (compile the game with -Dmain=gameMain, say, and call gameMain from appMain). The library's start-up creates a game input window whose content area is the game's screen at a whole scale; its frame wait yields until the next frame on the high-resolution clock. In a window the game shares the screen with the desktop, so there is no mode change, no direct palette hardware and no vertical retrace; the port converts the library's back buffer into the window each frame. On the whole screen (see The Whole Screen) the game has its own mode, its own palette and the vertical retrace, while the other programs run on.
    +The game becomes a DVX program with its own main loop: appMain calls the game's main() (compile the game with -Dmain=gameMain, say, and call gameMain from appMain). The library's start-up creates a game input window whose content area is the game's screen at a whole scale; its frame wait yields until the next frame on the high-resolution clock. In a window the game shares the screen with the desktop, so there is no mode change, no direct palette hardware, and no vertical retrace; the port converts the library's back buffer into the window each frame. On the whole screen (see The Whole Screen) the game has its own mode, its own palette, and the vertical retrace, while the other programs run on.
     
     .h2 Mapping a Hardware Layer to DVX
     
    @@ -437,4 +437,4 @@ The game becomes a DVX program with its own main loop: appMain calls the game's
     
     .h2 Checking a Port
     
    -The library's own drawing tests (hashes of the back buffer after each drawing call) should give the same answers as on DOS, since the drawing code is the same. Test in a 256-colour and a 16-bit desktop, with and without a sound card and a joystick, and with other programs running; End Task and exit() must leave DVX, the sound and the timer as they were.
    +The library's own drawing tests (hashes of the back buffer after each drawing call) should give the same answers as on DOS, since the drawing code is the same. Test in a 256-colour and a 16-bit desktop, with and without a sound card and a joystick, and with other programs running; End Task and exit() must leave DVX, the sound, and the timer as they were.
    diff --git a/src/libs/kpunch/libdvx/sdkGuide.dhs b/src/libs/kpunch/libdvx/sdkGuide.dhs
    index 9784ea49..eb213dc7 100644
    --- a/src/libs/kpunch/libdvx/sdkGuide.dhs
    +++ b/src/libs/kpunch/libdvx/sdkGuide.dhs
    @@ -45,7 +45,7 @@ The DVX SDK holds everything needed to build DVX modules outside the DVX source
       ----             --------
       newProject.sh    Creates a project from a template
       sdk.mk           Build rules every project Makefile includes
    -  bin/             dvxres (resources), dvxhlpc (help compiler), bascomp (BASIC compiler), dxeGen.sh (runs dxe3gen)
    +  bin/             dvxres (resources), dvxhlpc (help compiler), bascomp (BASIC compiler), mkfont (fonts a program carries), dxeGen.sh (runs dxe3gen)
       include/         Public C headers, one directory per library, plus include/basic for BASIC declaration files
       templates/       The project templates
       samples/basic/   Example BASIC projects
    @@ -54,7 +54,7 @@ The DVX SDK holds everything needed to build DVX modules outside the DVX source
     .h2 Requirements
     
     .list
    -.item A Linux host (the tools in bin/ are Linux programs; on DVX itself they are SYSTEM/DVXRES.EXE, SYSTEM/DVXHLPC.EXE and SYSTEM/BASCOMP.EXE).
    +.item A Linux host (the tools in bin/ are Linux programs; on DVX itself they are SYSTEM/DVXRES.EXE, SYSTEM/DVXHLPC.EXE, and SYSTEM/BASCOMP.EXE).
     .item The DJGPP cross compiler (i586-pc-msdosdjgpp-gcc and dxe3gen). Projects find it through DJGPP_PREFIX, from the environment or the make command line. Without it they use toolchains/djgpp of the DVX source tree the SDK sits in (SDK_DIR/../../toolchains/djgpp), which ./installToolchain.sh sets up.
     .endlist
     
    @@ -72,19 +72,19 @@ The DVX SDK holds everything needed to build DVX modules outside the DVX source
       library   A C library of shared functions (NAME.lib) with BASIC declarations
       basic     A DVX BASIC program (NAME.dbp, built to NAME.app)
       game      A C game (NAME.app): a 320x200 screen in a window, frame pacing, keyboard and joystick, sound effects and music
    -  basicgame A DVX BASIC game (NAME.dbp): a FRAMEWAIT loop, GameKeyDown, STICK, STRIG and PLAY
    +  basicgame A DVX BASIC game (NAME.dbp): a FRAMEWAIT loop, GameKeyDown, STICK, STRIG, and PLAY
       control   A control written in DVX BASIC (NAME.dbp with NAME.ctl, built to NAME.wgt) usable from C programs and DVX BASIC
     .endtable
     
     The two game templates are explained in Writing Games for DVX.
     
    -NAME is a letter followed by letters and digits, 8 characters at most, because it becomes a DOS file name. Files are named NAME with its first letter in lower case (camelCase, such as myApp); DVX shows it in upper case. The script refuses DOS device names (CON, PRN, AUX, NUL, COM1 and so on), the name of a widget DVX already has, and an existing directory. The project goes in DIR, or a new directory of that name in the current one.
    +NAME is a letter followed by letters and digits, 8 characters at most, because it becomes a DOS file name. Files are named NAME with its first letter in lower case (camelCase, such as myApp); DVX shows it in upper case. The script refuses DOS device names (CON, PRN, AUX, NUL, COM1, and so on), the name of a widget DVX already has, and an existing directory. The project goes in DIR, or a new directory of that name in the current one.
     
    -The template's names become yours everywhere: file names, identifiers in each case style (tplName, TplName, tplname, TPLNAME), help topic ids and the ABI stamp. The Makefile refers to the SDK by a relative path, so a project and the SDK can move together.
    +The template's names become yours everywhere: file names, identifiers in each case style (tplName, TplName, tplname, TPLNAME), help topic ids, and the ABI stamp. The Makefile refers to the SDK by a relative path, so a project and the SDK can move together.
     
     .h2 Building
     
    -Every project Makefile only names the SDK, the kind and the module; the rules come from sdk.mk:
    +Every project Makefile only names the SDK, the kind, and the module; the rules come from sdk.mk:
     
     .table
       Command                            Does
    @@ -99,7 +99,7 @@ Every .c file in the project directory is compiled with warnings as errors (-Wal
     
     .h2 Programs That Use the Network
     
    -The dvxnet library provides TCP/IP through the standard socket calls (socket, connect, send, recv, getaddrinfo, select and so on). Its headers are in include/net: dvxNet.h, plus sys/socket.h, netinet/in.h, netinet/tcp.h, arpa/inet.h, netdb.h, poll.h, sys/select.h, sys/ioctl.h and an errno.h that adds the socket error codes to the C library's. Because they stand in for headers of the same name, include/net is not on every module's path; a module that uses sockets adds one line to its Makefile, before the include of sdk.mk:
    +The dvxnet library provides TCP/IP through the standard socket calls (socket, connect, send, recv, getaddrinfo, select, and so on). Its headers are in include/net: dvxNet.h, plus sys/socket.h, netinet/in.h, netinet/tcp.h, arpa/inet.h, netdb.h, poll.h, sys/select.h, sys/ioctl.h, and an errno.h that adds the socket error codes to the C library's. Because they stand in for headers of the same name, include/net is not on every module's path; a module that uses sockets adds one line to its Makefile, before the include of sdk.mk:
     
     .code
       NET = 1
    @@ -162,19 +162,21 @@ The build makes hello.app and hello.hlp. The project holds:
       File         Purpose
       ----         -------
       hello.c      The program
    -  hello.res    Resources appended to hello.app: its icon, name, author, description and a greeting text
    +  hello.res    Resources appended to hello.app: its icon, name, author, description, and a greeting text
       icon32.bmp   The 32x32 icon Program Manager shows
       hello.dhs    The program's help, compiled to hello.hlp
    -  Makefile     Names the SDK, KIND = app and NAME = hello
    +  Makefile     Names the SDK, KIND = app, and NAME = hello
     .endtable
     
     .h2 How the Program Works
     
    -SHELL_APP_DESCRIPTOR(...) declares the exported appDescriptor: the program's name, whether it has its own main loop, whether several copies may run, its task stack size and priority. It also records the binary interface version the program was built for.
    +SHELL_APP_DESCRIPTOR(...) declares the exported appDescriptor: the program's name, whether it has its own main loop, whether several copies may run, its task stack size, and priority. It also records the binary interface version the program was built for.
     
    -DVX calls appMain(DxeAppContextT *ctx) once. The template is a callback-only program (hasMainLoop = false): appMain creates a window with a File and Help menu, a label and a button, and returns 0. From then on DVX calls the window's onClose and onMenu callbacks and the button's onClick. Destroying the program's last window ends it. A program that needs to keep working between events sets hasMainLoop = true, loops in appMain and calls tsYield() often (see the Clock program's source for an example).
    +DVX calls appMain(DxeAppContextT *ctx) once. The template is a callback-only program (hasMainLoop = false): appMain creates a window with a File and Help menu, a label, and a button, and returns 0. From then on DVX calls the window's onClose and onMenu callbacks and the button's onClick. Destroying the program's last window ends it. A program that needs to keep working between events sets hasMainLoop = true, loops in appMain, and calls tsYield() often (see the Clock program's source for an example).
     
    -ctx gives the program its paths: appPath (the .app file, used to read its own resources with dvxResOpen and dvxResRead), appDir, configDir (a writable directory for its settings) and helpFile (preset to hello.hlp beside hello.app).
    +ctx gives the program its paths: appPath (the .app file, used to read its own resources with dvxResOpen and dvxResRead), appDir, configDir (a writable directory for its settings), and helpFile (preset to hello.hlp beside hello.app).
    +
    +A program can carry fonts of its own as font resources, so its text looks the same whatever fonts were installed: add a line such as "title font TITLE.DFN" to hello.res (dvxres checks that the file is a DVX font). DVX adds the program's fonts when it starts and forgets them when it ends; other programs never see them, and where a family is installed too, the program's sizes and styles come first (dvxText.h, Fonts a Program Carries). Make a .DFN with bin/mkfont from a TrueType, Type 1, or bitmap font -- its manifest names the sizes, whether they are smoothed too, and the characters to keep (mkfont.c's header documents every key; one size and only the characters the program shows keep the file small) -- or copy one from DVX's FONTS folder. A .DFN carries the notice and licence of the font it was made from.
     
     .h2 Run
     
    @@ -217,7 +219,7 @@ The build makes badge.wgt and badge.hlp (a preview of both help sources compiled
       File         Purpose
       ----         -------
       badge.c      The widget: class, API table, BASIC interface, wgtRegister
    -  badge.h      The public API: BadgeApiT, the dvxBadgeApi() accessor and the wgtBadge... macros
    +  badge.h      The public API: BadgeApiT, the dvxBadgeApi() accessor, and the wgtBadge... macros
       badge.res    Resources: icon24 (the DVX BASIC toolbox icon) and name (its tool tip)
       icon24.bmp   The 24x24 toolbox icon
       badge.dhs    Developer help, added to the system help
    @@ -232,7 +234,7 @@ DVX loads every widget when it starts and calls its wgtRegister once. The templa
     .list
     .item A widget class (WidgetClassT): the paint and minimum-size handlers, and the shared text handlers (the widget's data starts with its text, so widgetAllocWithText creates it and WCLASS_HAS_TEXT frees it).
     .item An API table (BadgeApiT) under the name "badge". It starts with WgtApiHeaderT, filled by WGT_API_HEADER. Programs reach it through the macros in badge.h, which call wgtGetApi("badge", sizeof(BadgeApiT)).
    -.item A BASIC interface (WgtIfaceT): the control name Badge, a Style property, a Clear method and Click as the default event. The form runtime adds the common properties (Caption, Left, Top, Width, Height, Visible, Enabled...) and events itself. wgtIfaceSetType binds the interface to the class, so wgtGetIfaceOf and the generic calls (wgtGetProp, wgtSetProp, wgtCallMethod) find it from any badge widget.
    +.item A BASIC interface (WgtIfaceT): the control name Badge, a Style property, a Clear method, and Click as the default event. The form runtime adds the common properties (Caption, Left, Top, Width, Height, Visible, Enabled...) and events itself. wgtIfaceSetType binds the interface to the class, so wgtGetIfaceOf and the generic calls (wgtGetProp, wgtSetProp, wgtCallMethod) find it from any badge widget.
     .endlist
     
     DVX_ABI_STAMP(badge) at file scope records the binary interface version. Its argument must be the module's file name in lower case.
    @@ -243,7 +245,7 @@ Install it (make install DVX_DIR=/path/to/dvx puts badge.wgt and its help in WID
     
     .list
     .item In the DVX BASIC toolbox: draw a Badge on a form, set its Style in the property panel.
    -.item Usable from a C program: add CFLAGS=-I../badge to the program's make command (or Makefile), include badge.h and call wgtBadge(parent, "Text"). wgtGetApi returns NULL when the widget is not installed, so a program that can run without it checks dvxBadgeApi() before using the macros.
    +.item Usable from a C program: add CFLAGS=-I../badge to the program's make command (or Makefile), include badge.h, and call wgtBadge(parent, "Text"). wgtGetApi returns NULL when the widget is not installed, so a program that can run without it checks dvxBadgeApi() before using the macros.
     .endlist
     
     .note warning
    @@ -296,11 +298,11 @@ The build makes mathx.lib and mathx.hlp (a preview of the help). The project hol
     
     .h2 How the Library Works
     
    -DVX loads every library when it starts, before any widget or program, in the order the .dep files require. Every function that is not static is exported: a program, widget or library loaded later calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file.
    +DVX loads every library when it starts, before any widget or program, in the order the .dep files require. Every function that is not static is exported: a program, widget, or library loaded later calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file.
     
     BASIC programs add mathx.bas to their project. DECLARE LIBRARY looks up each function by exactly the name it declares, without a trailing $ (a $ marks a function returning a string), so mathxGreet$ calls the C function mathxGreet. Keep the declarations and the C functions in step; strings are passed as const char * and returned as const char *.
     
    -A string declared AS STRING is text and stops at the first zero byte. For bytes (network data, samples, encrypted data) declare BINARY STRING: each BINARY STRING parameter becomes two C parameters at its place, the bytes (const uint8_t *, a private copy valid during the call, followed by a zero byte) and their count (int32_t); a BINARY STRING result adds one last C parameter, int32_t *outLen, where the function stores the length of the bytes it returns (the runtime copies them right after the call, so a static buffer is fine; NULL, a negative length or a length never set give an empty string). The template's tplNameXor shows both:
    +A string declared AS STRING is text and stops at the first zero byte. For bytes (network data, samples, encrypted data) declare BINARY STRING: each BINARY STRING parameter becomes two C parameters at its place, the bytes (const uint8_t *, a private copy valid during the call, followed by a zero byte) and their count (int32_t); a BINARY STRING result adds one last C parameter, int32_t *outLen, where the function stores the length of the bytes it returns (the runtime copies them right after the call, so a static buffer is fine; NULL, a negative length, or a length never set give an empty string). The template's tplNameXor shows both:
     
     .code
       // tplNameXor$(BYVAL bytes AS BINARY STRING, BYVAL key AS LONG) AS BINARY STRING
    @@ -309,7 +311,7 @@ A string declared AS STRING is text and stops at the first zero byte. For bytes
     
     .h2 Run
     
    -Install it (LIBS/LOCAL/MATHX/ gets mathx.lib, mathx.dep and MATHX.DHS) and restart DVX. Build programs that use it with CFLAGS=-I../mathx.
    +Install it (LIBS/LOCAL/MATHX/ gets mathx.lib, mathx.dep, and MATHX.DHS) and restart DVX. Build programs that use it with CFLAGS=-I../mathx.
     
     .h2 Help
     
    @@ -349,11 +351,11 @@ The build compiles the project with bascomp into greeter.app and the help into g
       Makefile      KIND = basic
     .endtable
     
    -The project lists the SDK's include/basic/help.bas, which declares HelpView; Help > Contents calls HelpView "greeter.hlp", which opens the file beside the program. The project settings in the .dbp take the place of a C program's resource file.
    +The project lists the SDK's include/basic/help.bas, which declares HelpView; Help > Contents calls HelpView "greeter.hlp", which opens the file beside the program. The project settings in the .dbp take the place of a C program's resource file; fonts the program carries go in its [Fonts] section (Project > Properties in DVX BASIC), and the build makes them font resources.
     
     .h2 Run and Debug
     
    -make install puts greeter.app and greeter.hlp in APPS/LOCAL/GREETER/. You can also copy the project directory to the DVX machine and open greeter.dbp in DVX BASIC (File > Open Project...): Run > Run (F5) runs it, Run > Debug sets it going under the debugger with breakpoints (F9), stepping and the locals, watch and call stack windows, and File > Make Executable... builds the .app there. The DVX BASIC help describes the language and the IDE.
    +make install puts greeter.app and greeter.hlp in APPS/LOCAL/GREETER/. You can also copy the project directory to the DVX machine and open greeter.dbp in DVX BASIC (File > Open Project...): Run > Run (F5) runs it, Run > Debug sets it going under the debugger with breakpoints (F9), stepping, and the locals, watch, and call stack windows, and File > Make Executable... builds the .app there. The DVX BASIC help describes the language and the IDE.
     
     .h2 Next
     
    @@ -388,7 +390,7 @@ The build compiles the control project with bascomp into dial.wgt, a control fil
       Makefile     KIND = control
     .endtable
     
    -The control's Public properties, methods and events are what programs use; UserControl is its drawing surface. The DVX BASIC help (UserControls: Your Own Controls) describes the language side.
    +The control's Public properties, methods, and events are what programs use; UserControl is its drawing surface. The DVX BASIC help (UserControls: Your Own Controls) describes the language side.
     
     .h2 Install
     
    @@ -409,7 +411,7 @@ make install puts dial.wgt and DIAL.BHS in WIDGETS/LOCAL/DIAL/. When DVX starts
     
     .h1 Using BASIC Controls from C
     
    -A control written in DVX BASIC is an ordinary widget type once it is registered: it has a widget class, an interface (WgtIfaceT) with its properties, methods and events, and a type name equal to its BASIC name. A C program uses it through the generic widget access, without a header of its own:
    +A control written in DVX BASIC is an ordinary widget type once it is registered: it has a widget class, an interface (WgtIfaceT) with its properties, methods, and events, and a type name equal to its BASIC name. A C program uses it through the generic widget access, without a header of its own:
     
     .code
       WidgetT  *dial = wgtCreateByName("Dial", parent);
    @@ -428,7 +430,7 @@ A control written in DVX BASIC is an ordinary widget type once it is registered:
       }
     .endcode
     
    -Property values follow the BASIC types: Integer, Long and Byte are WGT_IFACE_INT, Boolean WGT_IFACE_BOOL, Single WGT_IFACE_FLOAT, Double and Currency WGT_IFACE_DOUBLE, String, Date and Variant WGT_IFACE_STRING. A property declared As an Enum whose members are numbered 0, 1, 2 ... (each number once) is WGT_IFACE_ENUM with the member names in value order (WgtPropDescT.enumNames; wgtSetProp takes the number or a member name, wgtIfaceGetPropText gives the name); an Enum with other values is WGT_IFACE_INT. A method takes its arguments as they come (WGT_SIG_DYNAMIC) and returns its Function's value, or no value (WGT_IFACE_NONE) for a Sub. An event's numeric and Boolean arguments that onEvent changes go back to the control (ByRef parameters). Events the control names Click, DblClick, Change, KeyPress, KeyDown, KeyUp, MouseDown, MouseUp, MouseMove or Scroll go to the matching universal callback (onClick ...) instead; the dispatcher calls those for a BASIC control only when its code raises them (WCLASS_RAISES_EVENTS). The interface's events list each event with its parameters as BASIC writes them (WgtEventDescT.params).
    +Property values follow the BASIC types: Integer, Long, and Byte are WGT_IFACE_INT, Boolean WGT_IFACE_BOOL, Single WGT_IFACE_FLOAT, Double and Currency WGT_IFACE_DOUBLE, String, Date, and Variant WGT_IFACE_STRING. A property declared As an Enum whose members are numbered 0, 1, 2 ... (each number once) is WGT_IFACE_ENUM with the member names in value order (WgtPropDescT.enumNames; wgtSetProp takes the number or a member name, wgtIfaceGetPropText gives the name); an Enum with other values is WGT_IFACE_INT. A method takes its arguments as they come (WGT_SIG_DYNAMIC) and returns its Function's value, or no value (WGT_IFACE_NONE) for a Sub. An event's numeric and Boolean arguments that onEvent changes go back to the control (ByRef parameters). Events the control names Click, DblClick, Change, KeyPress, KeyDown, KeyUp, MouseDown, MouseUp, MouseMove, or Scroll go to the matching universal callback (onClick ...) instead; the dispatcher calls those for a BASIC control only when its code raises them (WCLASS_RAISES_EVENTS). The interface's events list each event with its parameters as BASIC writes them (WgtEventDescT.params).
     
     wgtCreateByName goes through the interface's dynamic create hook (WgtIfaceT.dyn); the class has no C API structure (wgtGetApi finds none), because one create function cannot tell which BASIC class to make.
     
    @@ -450,7 +452,7 @@ The loader tells such a file from a module by its first bytes: instead of loadin
     
     .h2 The Bridge
     
    -Each control is one object of its BASIC class plus a border-less canvas, the UserControl surface its code draws on (the same drawing code as a BASIC PictureBox). The widget class forwards to the class's code: layout sizes the canvas, and between frames a new size runs UserControl_Resize and UserControl_Paint; the mouse, key, wheel and focus class methods run UserControl_MouseDown and the other handlers. A control file's code runs in one VM shared by every program that uses its controls; it never yields, and a call from another task while it runs code is refused (the paint keeps the old picture, the event is dropped). A BASIC program's own controls ([Controls] in its project) are registered by the program when it starts, owned by it, and run in its own VM.
    +Each control is one object of its BASIC class plus a border-less canvas, the UserControl surface its code draws on (the same drawing code as a BASIC PictureBox). The widget class forwards to the class's code: layout sizes the canvas, and between frames a new size runs UserControl_Resize and UserControl_Paint; the mouse, key, wheel, and focus class methods run UserControl_MouseDown and the other handlers. A control file's code runs in one VM shared by every program that uses its controls; it never yields, and a call from another task while it runs code is refused (the paint keeps the old picture, the event is dropped). A BASIC program's own controls ([Controls] in its project) are registered by the program when it starts, owned by it, and run in its own VM.
     
     .link sdk.firstcontrol Your First BASIC Control
     .link sdk.firstwidget Your First Widget
    diff --git a/src/libs/kpunch/libdvx/sysdoc.dhs b/src/libs/kpunch/libdvx/sysdoc.dhs
    index 7fe477c5..a254da86 100644
    --- a/src/libs/kpunch/libdvx/sysdoc.dhs
    +++ b/src/libs/kpunch/libdvx/sysdoc.dhs
    @@ -88,7 +88,7 @@ Use the table of contents on the left to navigate. The API reference is organise
     
     .h2 Getting Started
     
    -To write a program, widget or library right away, start with the SDK Quick Start: it creates a working project from a template and walks through building, running, debugging and installing it.
    +To write a program, widget, or library right away, start with the SDK Quick Start: it creates a working project from a template and walks through building, running, debugging, and installing it.
     
     .link sdk.quickstart SDK Quick Start
     
    diff --git a/src/libs/kpunch/libdvx/widgetCore.c b/src/libs/kpunch/libdvx/widgetCore.c
    index c384d19b..1146301e 100644
    --- a/src/libs/kpunch/libdvx/widgetCore.c
    +++ b/src/libs/kpunch/libdvx/widgetCore.c
    @@ -41,6 +41,7 @@
     
     #include "dvxWgtP.h"
     #include "dvxDraw.h"
    +#include "dvxText.h"
     #include "dvxPlat.h"
     #include "stb_ds_wrap.h"
     #include "../../../widgets/kpunch/timer/timer.h"
    @@ -346,7 +347,7 @@ void widgetAllocRollback(WidgetT *w) {
     
         widgetClearReferences(w);
     
    -    free(w);
    +    widgetFree(w);
     }
     
     
    @@ -543,7 +544,7 @@ void widgetDestroyChildren(WidgetT *w) {
     
             widgetClearReferences(child);
     
    -        free(child);
    +        widgetFree(child);
             child = next;
         }
     
    @@ -728,6 +729,13 @@ int32_t widgetFrameBorderWidth(const WidgetT *w) {
     }
     
     
    +// Frees a widget struct and closes the font it holds
    +void widgetFree(WidgetT *w) {
    +    dvxFontClose(w->font);
    +    free(w);
    +}
    +
    +
     // Recursive hit testing: finds the deepest (most specific) widget
     // under the given coordinates. Returns the widget itself if no
     // child is hit, or NULL if the point is outside this widget.
    diff --git a/src/libs/kpunch/libdvx/widgetLayout.c b/src/libs/kpunch/libdvx/widgetLayout.c
    index 29c743ad..393e6752 100644
    --- a/src/libs/kpunch/libdvx/widgetLayout.c
    +++ b/src/libs/kpunch/libdvx/widgetLayout.c
    @@ -64,7 +64,8 @@ static int32_t gapSizeOf(int32_t pixels);
     
     // Extra main-axis space a weighted box child can absorb before its
     // maxW/maxH cap, or -1 when it has no cap on the main axis.  0 when the
    -// cap is already at or below the child's minimum.
    +// cap is already at or below the child's minimum.  font is the box's: a
    +// cap in characters counts the child's own (wgtFontFor).
     static int32_t boxCapRoom(const WidgetT *c, bool horiz, int32_t innerW, int32_t innerH, const BitmapFontT *font) {
         int32_t maxTagged = horiz ? c->maxW : c->maxH;
     
    @@ -72,7 +73,7 @@ static int32_t boxCapRoom(const WidgetT *c, bool horiz, int32_t innerW, int32_t
             return -1;
         }
     
    -    int32_t maxPx = wgtResolveSize(maxTagged, horiz ? innerW : innerH, font->charWidth);
    +    int32_t maxPx = wgtResolveSize(maxTagged, horiz ? innerW : innerH, wgtFontFor(c, font)->charWidth);
     
         return DVX_MAX(maxPx - boxChildMin(c, horiz, innerW, innerH, font), 0);
     }
    @@ -87,9 +88,12 @@ static bool boxChildFrozen(const WidgetT *c, int32_t capRoom, int32_t freeExtra,
     
     
     // A box child's main-axis minimum, no larger than its main-axis cap (a
    -// percentage cap resolved against the box's inner size).
    +// percentage cap resolved against the box's inner size, one in characters
    +// in the child's own font; font is the box's).
     static int32_t boxChildMin(const WidgetT *c, bool horiz, int32_t innerW, int32_t innerH, const BitmapFontT *font) {
    -    return horiz ? capMin(c->calcMinW, c->maxW, innerW, font) : capMin(c->calcMinH, c->maxH, innerH, font);
    +    const BitmapFontT *cFont = wgtFontFor(c, font);
    +
    +    return horiz ? capMin(c->calcMinW, c->maxW, innerW, cFont) : capMin(c->calcMinH, c->maxH, innerH, cFont);
     }
     
     
    @@ -313,6 +317,8 @@ void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
     // applied as a ceiling (a percentage cap waits for the layout).
     
     void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
    +    const BitmapFontT *own = wgtFontFor(w, font);
    +
         if (wclsHas(w, WGT_METHOD_CALC_MIN_SIZE)) {
             wclsCalcMinSize(w, font);
         } else {
    @@ -325,7 +331,7 @@ void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
         // content is smaller. This is resolved at measure time (not arrange)
         // so parent containers see the correct minimum when distributing space.
         if (w->minW) {
    -        int32_t hintW = wgtResolveSize(w->minW, 0, font->charWidth);
    +        int32_t hintW = wgtResolveSize(w->minW, 0, own->charWidth);
     
             if (hintW > w->calcMinW) {
                 w->calcMinW = hintW;
    @@ -333,16 +339,17 @@ void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
         }
     
         if (w->minH) {
    -        int32_t hintH = wgtResolveSize(w->minH, 0, font->charWidth);
    +        int32_t hintH = wgtResolveSize(w->minH, 0, own->charWidth);
     
             if (hintH > w->calcMinH) {
                 w->calcMinH = hintH;
             }
         }
     
    -    // The cap wins over the minimum, as in the layout
    -    w->calcMinW = capMin(w->calcMinW, w->maxW, 0, font);
    -    w->calcMinH = capMin(w->calcMinH, w->maxH, 0, font);
    +    // The cap wins over the minimum, as in the layout (hints in
    +    // characters count the widget's own font)
    +    w->calcMinW = capMin(w->calcMinW, w->maxW, 0, own);
    +    w->calcMinH = capMin(w->calcMinH, w->maxH, 0, own);
     }
     
     
    @@ -487,6 +494,7 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
             }
     
             int32_t mainSize = boxChildMin(c, horiz, innerW, innerH, font);
    +        int32_t charW    = wgtFontFor(c, font)->charWidth;
     
             // Distribute extra space by weight
             if (totalWeight > 0 && c->weight > 0 && extraSpace > 0) {
    @@ -512,13 +520,13 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
     
             // Apply max size constraint
             if (horiz && c->maxW) {
    -            int32_t maxPx = wgtResolveSize(c->maxW, innerW, font->charWidth);
    +            int32_t maxPx = wgtResolveSize(c->maxW, innerW, charW);
     
                 if (mainSize > maxPx) {
                     mainSize = maxPx;
                 }
             } else if (!horiz && c->maxH) {
    -            int32_t maxPx = wgtResolveSize(c->maxH, innerH, font->charWidth);
    +            int32_t maxPx = wgtResolveSize(c->maxH, innerH, charW);
     
                 if (mainSize > maxPx) {
                     mainSize = maxPx;
    @@ -531,13 +539,13 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
             // Apply preferred size as cross-axis cap (e.g. Width in a VBox).
             // When set, the widget won't stretch beyond this on the cross axis.
             if (horiz && c->prefH) {
    -            int32_t prefPx = wgtResolveSize(c->prefH, innerH, font->charWidth);
    +            int32_t prefPx = wgtResolveSize(c->prefH, innerH, charW);
     
                 if (crossSize > prefPx) {
                     crossSize = prefPx;
                 }
             } else if (!horiz && c->prefW) {
    -            int32_t prefPx = wgtResolveSize(c->prefW, innerW, font->charWidth);
    +            int32_t prefPx = wgtResolveSize(c->prefW, innerW, charW);
     
                 if (crossSize > prefPx) {
                     crossSize = prefPx;
    @@ -546,13 +554,13 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
     
             // Apply max size on cross axis
             if (horiz && c->maxH) {
    -            int32_t maxPx = wgtResolveSize(c->maxH, innerH, font->charWidth);
    +            int32_t maxPx = wgtResolveSize(c->maxH, innerH, charW);
     
                 if (crossSize > maxPx) {
                     crossSize = maxPx;
                 }
             } else if (!horiz && c->maxW) {
    -            int32_t maxPx = wgtResolveSize(c->maxW, innerW, font->charWidth);
    +            int32_t maxPx = wgtResolveSize(c->maxW, innerW, charW);
     
                 if (crossSize > maxPx) {
                     crossSize = maxPx;
    diff --git a/src/libs/kpunch/libdvx/widgetOps.c b/src/libs/kpunch/libdvx/widgetOps.c
    index eb81b80d..2d33e06b 100644
    --- a/src/libs/kpunch/libdvx/widgetOps.c
    +++ b/src/libs/kpunch/libdvx/widgetOps.c
    @@ -34,6 +34,8 @@
     
     #include "dvxWgtP.h"
     #include "dvxPlat.h"
    +#include "dvxFont.h"
    +#include "dvxText.h"
     #include "stb_ds_wrap.h"
     #include "../../../widgets/kpunch/box/box.h"
     
    @@ -130,7 +132,7 @@ void wgtDestroy(WidgetT *w) {
             w->window->widgetRoot = NULL;
         }
     
    -    free(w);
    +    widgetFree(w);
     }
     
     
    @@ -181,6 +183,36 @@ static WidgetT *wgtFindImpl(WidgetT *w, const char *name) {
     }
     
     
    +// The font w draws with now: wgtFontFor with the application's font (the
    +// system font for a widget no window holds yet)
    +const BitmapFontT *wgtFont(const WidgetT *w) {
    +    AppContextT *ctx = wgtGetContext(w);
    +
    +    return wgtFontFor(w, ctx ? &ctx->font : &dvxFont8x16);
    +}
    +
    +
    +// The nearest own font up from w (NULL: none), else inherited (which may
    +// be NULL: a metrics query that needs no font); the system font in place
    +// of a proportional one for a class that edits by columns
    +const BitmapFontT *wgtFontFor(const WidgetT *w, const BitmapFontT *inherited) {
    +    const BitmapFontT *font = inherited;
    +
    +    for (const WidgetT *p = w; p; p = p->parent) {
    +        if (p->font) {
    +            font = p->font;
    +            break;
    +        }
    +    }
    +
    +    if (font && (font->flags & FONT_FLAG_PROPORTIONAL) && w && w->wclass && (w->wclass->flags & WCLASS_FIXED_FONT)) {
    +        return &dvxFont8x16;
    +    }
    +
    +    return font;
    +}
    +
    +
     // Retrieves the AppContextT from any widget.  The window's root widget
     // stores the context in its userData field (set during wgtInitWindow), so
     // an attached widget reaches it in O(1) through its window.  A widget
    @@ -399,6 +431,31 @@ void wgtSetFocused(WidgetT *w) {
     }
     
     
    +// A font of w's own (NULL family: none, so its container's again).  The
    +// new font is opened before the old one is given up, so a failed open
    +// leaves w as it was.
    +bool wgtSetFont(WidgetT *w, const char *family, int32_t size, uint32_t style) {
    +    BitmapFontT *font = NULL;
    +
    +    if (!w) {
    +        return false;
    +    }
    +
    +    if (family) {
    +        font = dvxFontOpen(family, size, style);
    +
    +        if (!font) {
    +            return false;
    +        }
    +    }
    +
    +    dvxFontClose(w->font);
    +    w->font = font;
    +    wgtInvalidate(w);
    +    return true;
    +}
    +
    +
     void wgtSetName(WidgetT *w, const char *name) {
         if (!w || !name) {
             return;
    @@ -690,6 +747,45 @@ void widgetPressableOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
     }
     
     
    +// A display onto w's window content buffer clipped to w and every
    +// ancestor, for painting w outside the paint walk (a quick repaint: the
    +// caller then invalidates only the rows it drew).  False when w cannot be
    +// painted that way now: hidden, clipped away entirely, or inside a scroll
    +// container, whose viewport is not its rect (w is invalidated for the
    +// normal paint then); the caller drops what it meant to draw.
    +bool widgetQuickPaintBegin(WidgetT *w, DisplayT *cd) {
    +    WindowT     *win = w->window;
    +    AppContextT *ctx = wgtGetContext(w);
    +    RectT        clip;
    +
    +    if (!win || !win->contentBuf || !ctx || !widgetIsShown(w)) {
    +        return false;
    +    }
    +
    +    clip.x = 0;
    +    clip.y = 0;
    +    clip.w = win->contentW;
    +    clip.h = win->contentH;
    +
    +    for (const WidgetT *p = w; p; p = p->parent) {
    +        if (p != w && p->wclass && (p->wclass->flags & WCLASS_SCROLL_CONTAINER)) {
    +            wgtInvalidatePaint(w);
    +            return false;
    +        }
    +
    +        RectT pr = { p->x, p->y, p->w, p->h };
    +
    +        if (!rectIntersect(&clip, &pr, &clip)) {
    +            return false;
    +        }
    +    }
    +
    +    dvxContentDisplay(ctx, win, cd);
    +    setClipRect(cd, clip.x, clip.y, clip.w, clip.h);
    +    return true;
    +}
    +
    +
     // Shared text getter for widgets whose DataT first field is
     // `const char *text`. Returns empty string when nothing is set so
     // callers never need a NULL check.
    diff --git a/src/libs/kpunch/listhelp/listHelp.c b/src/libs/kpunch/listhelp/listHelp.c
    index 7fbab5a7..6526a1c7 100644
    --- a/src/libs/kpunch/listhelp/listHelp.c
    +++ b/src/libs/kpunch/listhelp/listHelp.c
    @@ -58,16 +58,17 @@ int32_t         listIconScrollDrag(const ListIconGridT *g, int32_t y, int32_t gr
     ListIconScrollE listIconScrollPress(const ListIconGridT *g, int32_t x, int32_t y, int32_t *pScrollRow, int32_t *outGrab);
     int32_t         listIndexAfterMove(int32_t idx, int32_t from, int32_t dest);
     int32_t         listIndexAfterRemove(int32_t idx, int32_t removed);
    -bool            listItemsAdd(ListItemsT *li, const char *text);
    +bool            listItemsAdd(ListItemsT *li, const BitmapFontT *font, const char *text);
     void            listItemsAdopt(ListItemsT *li);
     bool            listItemsClear(ListItemsT *li);
     void            listItemsFree(ListItemsT *li);
     const char     *listItemsGet(const ListItemsT *li, int32_t idx);
    -bool            listItemsRemove(ListItemsT *li, int32_t idx);
    -bool            listItemsSetExternal(ListItemsT *li, const char **items, int32_t count);
    +int32_t         listItemsMaxWidth(ListItemsT *li, const BitmapFontT *font);
    +bool            listItemsRemove(ListItemsT *li, const BitmapFontT *font, int32_t idx);
    +bool            listItemsSetExternal(ListItemsT *li, const BitmapFontT *font, const char **items, int32_t count);
     static void     listItemsSync(ListItemsT *li);
     void            listPopupAddItem(WidgetT *w, ListPopupT *p, const char *text);
    -void            listPopupCalcMinSize(WidgetT *w, const BitmapFontT *font, const ListPopupT *p);
    +void            listPopupCalcMinSize(WidgetT *w, const BitmapFontT *font, ListPopupT *p);
     void            listPopupClearItems(WidgetT *w, ListPopupT *p);
     void            listPopupClose(WidgetT *w, ListPopupT *p);
     bool            listPopupCommit(ListPopupT *p);
    @@ -105,12 +106,13 @@ void            listSelReset(ListSelT *s, int32_t count);
     bool            listSelSetAll(ListSelT *s, int32_t count, bool selected);
     void            listSelSetCursor(ListSelT *s, int32_t count, int32_t idx);
     bool            listSelSetItem(ListSelT *s, int32_t count, int32_t idx, bool selected);
    +int32_t         listTextFit(const BitmapFontT *font, const char *text, int32_t maxW);
     char          **widgetAdoptOwnedStrings(char **owned, const char **src, int32_t count);
     void            widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color);
     void            widgetDropdownPopupRect(const WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t itemCount, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH);
     void            widgetFreeOwnedStrings(char **owned);
     int32_t         widgetGridNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t columns, int32_t pageRows);
    -int32_t         widgetMaxItemLen(const char **items, int32_t count);
    +int32_t         widgetMaxItemWidth(const BitmapFontT *font, const char **items, int32_t count);
     int32_t         widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize);
     void            widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos);
     bool            widgetPopupScrollbarClick(int32_t x, int32_t y, int32_t popX, int32_t popY, int32_t popW, int32_t popH, int32_t itemCount, int32_t visibleItems, int32_t *scrollPos);
    @@ -471,16 +473,17 @@ int32_t listIndexAfterRemove(int32_t idx, int32_t removed) {
     }
     
     
    -bool listItemsAdd(ListItemsT *li, const char *text) {
    -    const char *s   = text ? text : "";
    -    int32_t     len = (int32_t)strlen(s);
    +bool listItemsAdd(ListItemsT *li, const BitmapFontT *font, const char *text) {
    +    const char *s      = text ? text : "";
    +    int32_t     oldMax = listItemsMaxWidth(li, font);
    +    int32_t     width  = textWidth(font, s);
     
         listItemsAdopt(li);
         arrput(li->owned, strdup(s));
         listItemsSync(li);
     
    -    if (len > li->maxLen) {
    -        li->maxLen = len;
    +    if (width > oldMax) {
    +        li->maxW = width;
             return true;
         }
     
    @@ -500,12 +503,13 @@ void listItemsAdopt(ListItemsT *li) {
     }
     
     
    +// An empty list is 0 wide in any font, so the measured font stays.
     bool listItemsClear(ListItemsT *li) {
    -    int32_t oldMax = li->maxLen;
    +    int32_t oldMax = li->maxW;
     
         widgetFreeOwnedStrings(li->owned);
         listItemsSync(li);
    -    li->maxLen = 0;
    +    li->maxW = 0;
         return oldMax != 0;
     }
     
    @@ -526,37 +530,53 @@ const char *listItemsGet(const ListItemsT *li, int32_t idx) {
     }
     
     
    +// The widest item's width in font: the cached width when it was measured
    +// in font, else every item is measured again.
    +int32_t listItemsMaxWidth(ListItemsT *li, const BitmapFontT *font) {
    +    if (!fontStampIs(&li->widthFont, font)) {
    +        li->maxW = widgetMaxItemWidth(font, li->items, li->count);
    +        fontStampSet(&li->widthFont, font);
    +    }
    +
    +    return li->maxW;
    +}
    +
    +
     // Remove item idx (ignored when out of range).  The widest-item cache is
     // only rescanned when the removed item was as wide as the widest.
    -bool listItemsRemove(ListItemsT *li, int32_t idx) {
    +bool listItemsRemove(ListItemsT *li, const BitmapFontT *font, int32_t idx) {
         if (idx < 0 || idx >= li->count) {
             return false;
         }
     
    +    int32_t oldMax = listItemsMaxWidth(li, font);
    +
         listItemsAdopt(li);
     
         // An owned entry is NULL when its strdup failed
    -    int32_t len    = li->owned[idx] ? (int32_t)strlen(li->owned[idx]) : 0;
    -    int32_t oldMax = li->maxLen;
    +    int32_t width = li->owned[idx] ? textWidth(font, li->owned[idx]) : 0;
     
         free(li->owned[idx]);
         arrdel(li->owned, idx);
         listItemsSync(li);
     
    -    if (len < oldMax) {
    +    if (width < oldMax) {
             return false;
         }
     
    -    li->maxLen = widgetMaxItemLen(li->items, li->count);
    -    return li->maxLen != oldMax;
    +    li->maxW = widgetMaxItemWidth(font, li->items, li->count);
    +    return li->maxW != oldMax;
     }
     
     
     // Point the list at a caller-owned array.  Previously owned strings are
     // kept (the new array may point into them) until the next adopt, clear or
    -// free.
    -bool listItemsSetExternal(ListItemsT *li, const char **items, int32_t count) {
    -    int32_t oldMax = li->maxLen;
    +// free.  The old items are not measured again (the caller may have freed
    +// its old array), so a width measured in another font counts as changed;
    +// a list never measured has never held items (0 wide in any font).
    +bool listItemsSetExternal(ListItemsT *li, const BitmapFontT *font, const char **items, int32_t count) {
    +    bool    sameFont = !li->widthFont.font || fontStampIs(&li->widthFont, font);
    +    int32_t oldMax   = li->maxW;
     
         if (!items || count < 0) {
             count = 0;
    @@ -565,8 +585,9 @@ bool listItemsSetExternal(ListItemsT *li, const char **items, int32_t count) {
         li->items   = items;
         li->count   = count;
         li->isOwned = false;
    -    li->maxLen  = widgetMaxItemLen(items, count);
    -    return li->maxLen != oldMax;
    +    li->maxW    = widgetMaxItemWidth(font, items, count);
    +    fontStampSet(&li->widthFont, font);
    +    return !sameFont || li->maxW != oldMax;
     }
     
     
    @@ -580,14 +601,14 @@ static void listItemsSync(ListItemsT *li) {
     
     
     void listPopupAddItem(WidgetT *w, ListPopupT *p, const char *text) {
    -    listPopupItemsChanged(w, p, listItemsAdd(&p->items, text));
    +    listPopupItemsChanged(w, p, listItemsAdd(&p->items, wgtFont(w), text));
     }
     
     
     // Width: widest item (at least DROPDOWN_MIN_CHARS) + drop button + padding
     // + text-area bevel.  Height: one text row + padding.
    -void listPopupCalcMinSize(WidgetT *w, const BitmapFontT *font, const ListPopupT *p) {
    -    int32_t itemW = p->items.maxLen * font->charWidth;
    +void listPopupCalcMinSize(WidgetT *w, const BitmapFontT *font, ListPopupT *p) {
    +    int32_t itemW = listItemsMaxWidth(&p->items, font);
         int32_t minW  = DROPDOWN_MIN_CHARS * font->charWidth;
     
         if (itemW < minW) {
    @@ -764,8 +785,7 @@ bool listPopupNavKey(ListPopupT *p, int32_t key) {
     // open; anything else closes it (before the caller notifies, since the
     // handler may destroy the widget) and an item click selects that item.
     ListPopupHitE listPopupOnMouse(WidgetT *w, ListPopupT *p, int32_t vx, int32_t vy) {
    -    AppContextT       *ctx  = wgtGetContext(w);
    -    const BitmapFontT *font = &ctx->font;
    +    const BitmapFontT *font = wgtFont(w);
         int32_t            popX;
         int32_t            popY;
         int32_t            popW;
    @@ -820,7 +840,7 @@ void listPopupRemoveItem(WidgetT *w, ListPopupT *p, int32_t idx) {
             return;
         }
     
    -    bool widthChanged = listItemsRemove(&p->items, idx);
    +    bool widthChanged = listItemsRemove(&p->items, wgtFont(w), idx);
     
         p->selectedIdx = listIndexAfterRemove(p->selectedIdx, idx);
         p->hoverIdx    = listIndexAfterRemove(p->hoverIdx, idx);
    @@ -829,7 +849,7 @@ void listPopupRemoveItem(WidgetT *w, ListPopupT *p, int32_t idx) {
     
     
     void listPopupSetItems(WidgetT *w, ListPopupT *p, const char **items, int32_t count) {
    -    bool widthChanged = listItemsSetExternal(&p->items, items, count);
    +    bool widthChanged = listItemsSetExternal(&p->items, wgtFont(w), items, count);
     
         if (p->selectedIdx >= p->items.count) {
             p->selectedIdx = -1;
    @@ -1149,6 +1169,31 @@ bool listSelSetItem(ListSelT *s, int32_t count, int32_t idx, bool selected) {
     }
     
     
    +// Every character of a fixed-pitch font is one cell wide (the system
    +// font and a loaded fixed-pitch one alike), so strnlen bounds the scan to
    +// the cells that fit; a proportional font sums the characters' advances.
    +int32_t listTextFit(const BitmapFontT *font, const char *text, int32_t maxW) {
    +    if (!(font->flags & FONT_FLAG_PROPORTIONAL)) {
    +        return (int32_t)strnlen(text, (size_t)(DVX_MAX(maxW, 0) / font->charWidth));
    +    }
    +
    +    int32_t count = 0;
    +    int32_t width = 0;
    +
    +    while (text[count]) {
    +        width += textWidthN(font, text + count, 1);
    +
    +        if (width > maxW) {
    +            break;
    +        }
    +
    +        count++;
    +    }
    +
    +    return count;
    +}
    +
    +
     // Rebuild an internally-owned stb_ds string array as a deep copy of an
     // external source array.  The copy is complete before the old strings are
     // freed, so src may alias them.  Returns the new owned array.
    @@ -1246,20 +1291,20 @@ int32_t widgetGridNavigateIndex(int32_t key, int32_t current, int32_t count, int
     }
     
     
    -// Scans an array of string items and returns the maximum strlen.  NULL
    -// entries count as empty.
    -int32_t widgetMaxItemLen(const char **items, int32_t count) {
    -    int32_t maxLen = 0;
    +// Scans an array of string items and returns the widest one's width in
    +// font.  NULL entries count as empty.
    +int32_t widgetMaxItemWidth(const BitmapFontT *font, const char **items, int32_t count) {
    +    int32_t maxW = 0;
     
         for (int32_t i = 0; items && i < count; i++) {
    -        int32_t slen = items[i] ? (int32_t)strlen(items[i]) : 0;
    +        int32_t width = items[i] ? textWidth(font, items[i]) : 0;
     
    -        if (slen > maxLen) {
    -            maxLen = slen;
    +        if (width > maxW) {
    +            maxW = width;
             }
         }
     
    -    return maxLen;
    +    return maxW;
     }
     
     
    diff --git a/src/libs/kpunch/listhelp/listHelp.h b/src/libs/kpunch/listhelp/listHelp.h
    index 13ab354a..276c88bc 100644
    --- a/src/libs/kpunch/listhelp/listHelp.h
    +++ b/src/libs/kpunch/listhelp/listHelp.h
    @@ -54,26 +54,30 @@
     // at a caller-owned SetItems array (isOwned false) or at owned, an stb_ds
     // array of strdup'd strings (isOwned true).  AddItem/RemoveItem/Clear
     // first adopt an external array (deep copy) so they extend the caller's
    -// list instead of replacing it.  maxLen caches the widest item in
    -// characters and is maintained incrementally.  NULL entries in an external
    -// array read as empty strings.
    +// list instead of replacing it.  maxW caches the widest item's width in
    +// pixels of widthFont (zeroed: not measured yet); listItemsMaxWidth measures
    +// it again in another font and the mutators keep it current.  NULL
    +// entries in an external array read as empty strings.
     typedef struct {
    -    const char **items;
    -    char       **owned;
    -    int32_t      count;
    -    int32_t      maxLen;
    -    bool         isOwned;
    +    const char        **items;
    +    char              **owned;
    +    FontStampT          widthFont;
    +    int32_t             count;
    +    int32_t             maxW;
    +    bool                isOwned;
     } ListItemsT;
     
    -// Mutators return true when maxLen changed (the widget's minimum width
    -// changed, so it needs a relayout rather than just a repaint).
    -bool        listItemsAdd(ListItemsT *li, const char *text);
    +// Mutators measure in font (the widget's: wgtFont) and return true when
    +// the widest item's width changed (the widget's minimum width changed, so
    +// it needs a relayout rather than just a repaint).
    +bool        listItemsAdd(ListItemsT *li, const BitmapFontT *font, const char *text);
     void        listItemsAdopt(ListItemsT *li);
     bool        listItemsClear(ListItemsT *li);
     void        listItemsFree(ListItemsT *li);
     const char *listItemsGet(const ListItemsT *li, int32_t idx);
    -bool        listItemsRemove(ListItemsT *li, int32_t idx);
    -bool        listItemsSetExternal(ListItemsT *li, const char **items, int32_t count);
    +int32_t     listItemsMaxWidth(ListItemsT *li, const BitmapFontT *font);
    +bool        listItemsRemove(ListItemsT *li, const BitmapFontT *font, int32_t idx);
    +bool        listItemsSetExternal(ListItemsT *li, const BitmapFontT *font, const char **items, int32_t count);
     
     // Rebuild an internally-owned stb_ds string array as a deep copy of an
     // external source array.  The copy is built before the old strings are
    @@ -162,7 +166,7 @@ typedef enum {
     } ListPopupHitE;
     
     void          listPopupAddItem(WidgetT *w, ListPopupT *p, const char *text);
    -void          listPopupCalcMinSize(WidgetT *w, const BitmapFontT *font, const ListPopupT *p);
    +void          listPopupCalcMinSize(WidgetT *w, const BitmapFontT *font, ListPopupT *p);
     void          listPopupClearItems(WidgetT *w, ListPopupT *p);
     void          listPopupClose(WidgetT *w, ListPopupT *p);
     bool          listPopupCommit(ListPopupT *p);
    @@ -186,10 +190,15 @@ int32_t       listPopupVisibleCount(int32_t itemCount);
     void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color);
     
     // ============================================================
    -// Item measurement
    +// Text measurement
     // ============================================================
     
    -int32_t widgetMaxItemLen(const char **items, int32_t count);
    +// How many leading characters of text (up to its terminator) fit whole in
    +// maxW pixels of font: what a list draws of an item cut to its column.
    +int32_t listTextFit(const BitmapFontT *font, const char *text, int32_t maxW);
    +// The width in font of the widest of count items (NULL entries count as
    +// empty).
    +int32_t widgetMaxItemWidth(const BitmapFontT *font, const char **items, int32_t count);
     
     // ============================================================
     // Keyboard navigation (Up/Down/Home/End/PgUp/PgDn)
    diff --git a/src/libs/kpunch/listhelp/listhelp.dhs b/src/libs/kpunch/listhelp/listhelp.dhs
    index 1a8a67ea..f743231c 100644
    --- a/src/libs/kpunch/listhelp/listhelp.dhs
    +++ b/src/libs/kpunch/listhelp/listhelp.dhs
    @@ -26,7 +26,8 @@
     .toc 0 List Helper Library
     .index listhelp
     .index widgetDrawDropdownArrow
    -.index widgetMaxItemLen
    +.index widgetMaxItemWidth
    +.index listTextFit
     .index widgetNavigateIndex
     .index widgetGridNavigateIndex
     .index widgetDropdownPopupRect
    @@ -41,7 +42,7 @@
     
     .h1 List Helper Library
     
    -Shared helper routines for dropdown and list-based widget DXEs (ListBox, Dropdown, ComboBox, ListView). Provides the owned-string item list, the single/multi-select model, drag-reorder helpers, the dropdown popup (state, geometry, painting, hit testing and keyboard handling), dropdown arrow rendering, item measurement, keyboard navigation, type-ahead search, popup scrollbar hit testing (with the same thumb geometry the window manager paints), and the icon view rules of Program Manager's view, which BASIC icon views share through listhelp.bas.
    +Shared helper routines for dropdown and list-based widget DXEs (ListBox, Dropdown, ComboBox, ListView). Provides the owned-string item list, the single/multi-select model, drag-reorder helpers, the dropdown popup (state, geometry, painting, hit testing, and keyboard handling), dropdown arrow rendering, text measurement in any font (fixed-pitch or proportional), keyboard navigation, type-ahead search, popup scrollbar hit testing (with the same thumb geometry the window manager paints), and the icon view rules of Program Manager's view, which BASIC icon views share through listhelp.bas.
     
     Header: listhelp/listHelp.h
     
    @@ -64,27 +65,29 @@ Loaded as: bin/libs/listhelp.lib
     
     .h2 ListItemsT (owned-string item list)
     
    -Item list shared by ListBox, Dropdown and ComboBox. items points either at a caller-owned array (isOwned false) or at owned, an stb_ds array of strdup'd strings (isOwned true). maxLen caches the widest item in characters and is maintained incrementally. NULL entries in a caller's array read as empty strings.
    +Item list shared by ListBox, Dropdown, and ComboBox. items points either at a caller-owned array (isOwned false) or at owned, an stb_ds array of strdup'd strings (isOwned true). maxW caches the widest item's width in pixels of widthFont (NULL until it is first measured); listItemsMaxWidth returns it, measuring every item again when asked for another font, and the mutators keep it current incrementally. NULL entries in a caller's array read as empty strings.
     
     .code
     typedef struct {
    -    const char **items;
    -    char       **owned;
    -    int32_t      count;
    -    int32_t      maxLen;
    -    bool         isOwned;
    +    const char        **items;
    +    char              **owned;
    +    const BitmapFontT  *widthFont;
    +    int32_t             count;
    +    int32_t             maxW;
    +    bool                isOwned;
     } ListItemsT;
     
    -bool        listItemsAdd(ListItemsT *li, const char *text);
    +bool        listItemsAdd(ListItemsT *li, const BitmapFontT *font, const char *text);
     void        listItemsAdopt(ListItemsT *li);
     bool        listItemsClear(ListItemsT *li);
     void        listItemsFree(ListItemsT *li);
     const char *listItemsGet(const ListItemsT *li, int32_t idx);
    -bool        listItemsRemove(ListItemsT *li, int32_t idx);
    -bool        listItemsSetExternal(ListItemsT *li, const char **items, int32_t count);
    +int32_t     listItemsMaxWidth(ListItemsT *li, const BitmapFontT *font);
    +bool        listItemsRemove(ListItemsT *li, const BitmapFontT *font, int32_t idx);
    +bool        listItemsSetExternal(ListItemsT *li, const BitmapFontT *font, const char **items, int32_t count);
     .endcode
     
    -listItemsAdd and listItemsRemove first adopt (deep-copy) a caller's array, so they extend or shorten it without modifying it. listItemsSetExternal keeps the previously owned strings until the next adopt, clear or free, because the new array may point into them. The mutators return true when maxLen changed, i.e. when the widget's minimum width changed and it needs a relayout rather than just a repaint. listItemsGet returns "" for an index out of range.
    +listItemsAdd and listItemsRemove first adopt (deep-copy) a caller's array, so they extend or shorten it without modifying it. listItemsSetExternal keeps the previously owned strings until the next adopt, clear, or free, because the new array may point into them, and never reads the caller's previous array (it may be freed already). The mutators measure in font, the widget's own (wgtFont), and return true when the widest item's width changed, i.e. when the widget's minimum width changed and it needs a relayout rather than just a repaint. listItemsGet returns "" for an index out of range.
     
     widgetAdoptOwnedStrings(owned, src, count) builds the deep copy that adoption uses (complete before the old strings are freed, so src may alias them) and widgetFreeOwnedStrings(owned) frees every string of an owned array and empties it. ListView uses both for its row-major cell copy.
     
    @@ -109,7 +112,7 @@ typedef struct {
       listSelInsert(s, newCount, idx)                   An item was inserted at idx: existing selections are kept, cursor/anchor shift.
       listSelRemove(s, oldCount, idx)                   The item at idx was removed: bits close the gap, cursor/anchor shift; a removed cursor/anchor becomes -1.
       listSelReorder(s, from, dest)                     Drag-reorder moved an item: its bit moves with it and it becomes the cursor.
    -  listSelClick(s, count, order, disp, shift, ctrl)  Mouse selection: plain, Ctrl (toggle) or Shift (range from the anchor).
    +  listSelClick(s, count, order, disp, shift, ctrl)  Mouse selection: plain, Ctrl (toggle), or Shift (range from the anchor).
       listSelMove(s, count, order, disp, shift)         Keyboard cursor move; Shift selects the range from the anchor.
       listSelKey(s, count, key, mod)                    Multi-select Ctrl+A (ListSelKeyAllE) and Space (ListSelKeyToggleE); ListSelKeyNoneE otherwise.
       listSelSetCursor(s, count, idx)                   Programmatic selection: idx becomes the only selected item.
    @@ -134,7 +137,7 @@ listReorderCancel abandons an item drag when the list changes under it (an item
     
     .h2 ListPopupT (dropdown popup)
     
    -Popup state shared by Dropdown and ComboBox: the item list plus the selected item, the hovered item, the popup scroll position and whether it is open.
    +Popup state shared by Dropdown and ComboBox: the item list plus the selected item, the hovered item, the popup scroll position, and whether it is open.
     
     .code
     typedef struct {
    @@ -152,15 +155,15 @@ typedef struct {
       listPopupInit(p)                                  No selection, no hover, closed.
       listPopupAddItem / listPopupRemoveItem            Item list edits; the selection and hover stay on the same item (-1 when it is removed).
       listPopupClearItems / listPopupSetItems           Replace the items; an out-of-range selection becomes -1.
    -  listPopupOpen(w, p)                               Open with the selection hovered and scrolled into view (keyboard, mouse and accelerator all use it).
    +  listPopupOpen(w, p)                               Open with the selection hovered and scrolled into view (keyboard, mouse, and accelerator all use it).
       listPopupClose(w, p)                              WGT_METHOD_CLOSE_POPUP body.
       listPopupCommit(p)                                Close and make the hovered item the selection; false when nothing valid is hovered.
       listPopupNavKey(p, key)                           Up/Down move the hover item, a printable key jumps to the next matching item.
    -  listPopupOnMouse(w, p, vx, vy)                    Click while open: ListPopupHitScrollE (scrollbar), ListPopupHitItemE (item selected, closed) or ListPopupHitNoneE (closed).
    +  listPopupOnMouse(w, p, vx, vy)                    Click while open: ListPopupHitScrollE (scrollbar), ListPopupHitItemE (item selected, closed), or ListPopupHitNoneE (closed).
       listPopupItemAt(p, font, popY, vy)                Item under vy, or -1 on the border or past the last item.
       listPopupPaint(w, p, ...)                         Overlay paint; uses the window content height, the same source as the hit test.
       listPopupDrawFrame(d, ops, colors, w, bg, down)   Sunken text area plus the drop button and arrow.
    -  listPopupCalcMinSize(w, font, p)                  Minimum size from the widest item.
    +  listPopupCalcMinSize(w, font, p)                  Minimum size from the widest item in font.
       listPopupEnsureVisible(p)                         Scroll the hover item into view.
       listPopupVisibleCount(itemCount)                  Rows the popup shows (1..DROPDOWN_MAX_VISIBLE).
     .endtable
    @@ -183,22 +186,31 @@ void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX,
       color       Pre-packed pixel color for the arrow.
     .endtable
     
    -.h2 widgetMaxItemLen
    +.h2 widgetMaxItemWidth
     
    -Scan an array of strings and return the length (in characters) of the longest item.
    +Scan an array of strings and return the width in pixels of the widest item in a font.
     
     .code
    -int32_t widgetMaxItemLen(const char **items, int32_t count);
    +int32_t widgetMaxItemWidth(const BitmapFontT *font, const char **items, int32_t count);
     .endcode
     
     .table
       Parameter   Description
       ---------   -----------
    +  font        Font the items are drawn in (fixed-pitch or proportional).
       items       Array of null-terminated string pointers (NULL entries count as empty).
       count       Number of items in the array.
     .endtable
     
    -Returns the character length of the longest item, or 0 if count is zero.
    +Returns the width of the widest item, or 0 if count is zero.
    +
    +.h2 listTextFit
    +
    +How many leading characters of text fit whole in maxW pixels of font: the part of an item a list draws when the item is cut to its column. Stops at the end of the text; 0 when maxW is too narrow for the first character.
    +
    +.code
    +int32_t listTextFit(const BitmapFontT *font, const char *text, int32_t maxW);
    +.endcode
     
     .h2 widgetNavigateIndex
     
    @@ -221,7 +233,7 @@ Returns the new index, clamped to [0, count-1]. Returns -1 for unrecognized keys
     
     .h2 widgetGridNavigateIndex
     
    -The same keys in a grid of count items laid out row by row, columns items per row (icon views): Left and Right step through the items in order, Up, Down, Page Up and Page Down move by rows (pageRows rows a page) the way widgetNavigateIndex moves through a list -- it is called on the row numbers -- keeping the column, or landing on the last item when the row below is shorter; Home and End go to the first and last item. With nothing selected (current below 0) every one of these keys goes to the first item. With one column it gives what widgetNavigateIndex gives.
    +The same keys in a grid of count items laid out row by row, columns items per row (icon views): Left and Right step through the items in order, Up, Down, Page Up, and Page Down move by rows (pageRows rows a page) the way widgetNavigateIndex moves through a list -- it is called on the row numbers -- keeping the column, or landing on the last item when the row below is shorter; Home and End go to the first and last item. With nothing selected (current below 0) every one of these keys goes to the first item. With one column it gives what widgetNavigateIndex gives.
     
     .code
     int32_t widgetGridNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t columns, int32_t pageRows);
    @@ -330,7 +342,7 @@ Returns true if the click landed on the scrollbar (even if scrollPos was not cha
     
     .h2 Icon Views
     
    -The rules of an icon view with large icons, small icons or a list: Program Manager's view (progman/pmView.c) is built on them, and so are icon views written in BASIC (listhelp.bas ListIcon...), so both look and behave alike. They cover each view's cell and where an item's picture and name go in it, the grid inside a sunken frame with a scroll bar only when the rows do not fit (which narrows the grid), the two label lines under a large icon, the arrow keys among items in order or in cells of their own (gaps allowed), the scroll bar (a control's, as ListBox's: arrows, trough, thumb drag), when a press becomes a drag, the scrolling and the target cell while dragging, and where a dropped item lands. Positions are pixels from the view's top left (ListIconGridT) or the cell's (ListIconCellT).
    +The rules of an icon view with large icons, small icons, or a list: Program Manager's view (progman/pmView.c) is built on them, and so are icon views written in BASIC (listhelp.bas ListIcon...), so both look and behave alike. They cover each view's cell and where an item's picture and name go in it, the grid inside a sunken frame with a scroll bar only when the rows do not fit (which narrows the grid), the two label lines under a large icon, the arrow keys among items in order or in cells of their own (gaps allowed), the scroll bar (a control's, as ListBox's: arrows, trough, thumb drag), when a press becomes a drag, the scrolling and the target cell while dragging, and where a dropped item lands. Positions are pixels from the view's top left (ListIconGridT) or the cell's (ListIconCellT).
     
     .table
       Constant                 Value  Description
    @@ -407,7 +419,7 @@ int32_t listIconScrollDrag(const ListIconGridT *g, int32_t y, int32_t grab);
       listIconGridMetric     One value of a grid by ListIconMetricE (ListIconCellWE ... ListIconWheelRowsE; the scroll bar as 1 / 0; 0 for an unknown metric). The BASIC binding reads the grid through it.
       listIconCellAt         The cell number (row * cols + col) under x, y of a view scrolled to scrollRow, with its column and row; LIST_ICON_NO_CELL outside the grid or right of the last column.
       listIconLabelSplit     A large icon's two label lines maxChars wide: the first ends at the last blank that fits (else it is cut at maxChars), the second starts after the blanks there and is cut to maxChars. *line2 points into name; *len2 is 0 when the name fits one line.
    -  listIconNavigate       The item an arrow key, Home, End, PgUp or PgDn moves to when item i stands in cell (cols[i], rows[i]): Left / Right the nearest before / after in reading order, Up / Down the nearest row above / below with the closest column, PgUp / PgDn the same pageRows away, Home / End the first / last in reading order; with no current item the first. For items in order it gives what widgetGridNavigateIndex gives. -1 for another key or when no item is that way.
    +  listIconNavigate       The item an arrow key, Home, End, PgUp, or PgDn moves to when item i stands in cell (cols[i], rows[i]): Left / Right the nearest before / after in reading order, Up / Down the nearest row above / below with the closest column, PgUp / PgDn the same pageRows away, Home / End the first / last in reading order; with no current item the first. For items in order it gives what widgetGridNavigateIndex gives. -1 for another key or when no item is that way.
       listIconDragStarted    True when a press has moved LIST_ICON_DRAG_SLOP pixels either way.
       listIconDragScroll     The top row after a drag move to y: one row up above the grid, one down below it.
       listIconDropCell       The cell a dragged item would drop on with the pointer at x, y, held inside the grid's height (listIconCellAt).
    @@ -416,6 +428,8 @@ int32_t listIconScrollDrag(const ListIconGridT *g, int32_t y, int32_t grab);
       listIconDropDest       Items in order: the index an item dragged from `from` takes when dropped on cell number cell (that cell's, the last for a cell past the end); -1 when that changes nothing or from / cell is out of range.
     .endtable
     
    +charW and charH are the caller's character metrics (a font's charWidth and charHeight, or numbers from BASIC, which has no font handle here): rows are a line tall, and a name field or column N characters wide is N * charW. A large icon's label is cut by characters (nameChars, listIconLabelSplit): exact for a fixed-pitch font, an estimate for a proportional one.
    +
     Program Manager's pmView.c draws the scroll bar with widgetDrawScrollbarV; a BASIC icon view draws the same bar with DrawScrollBar's control argument True.
     
     .topic lib.listhelp.basic
    @@ -459,4 +473,4 @@ src/include/basic/listhelp.bas gives BASIC programs -- above all UserControls th
       ListIconScrollDrag(mode, cw, ch, w, h, count, rowsTaken, grab, y)  listIconScrollDrag.
     .endtable
     
    -An icon view's grid is figured from the view's size on each call (no handle). The LISTSEL_KEY_ and LISTICON_ CONSTs of listhelp.bas are compared with ListSelKeyE, ListIconViewE, LIST_ICON_BY_ORDER and ListIconMetricE by a formrt test.
    +An icon view's grid is figured from the view's size on each call (no handle). The LISTSEL_KEY_ and LISTICON_ CONSTs of listhelp.bas are compared with ListSelKeyE, ListIconViewE, LIST_ICON_BY_ORDER, and ListIconMetricE by a formrt test.
    diff --git a/src/libs/kpunch/music/music.c b/src/libs/kpunch/music/music.c
    index d7704b83..3cf7674b 100644
    --- a/src/libs/kpunch/music/music.c
    +++ b/src/libs/kpunch/music/music.c
    @@ -105,7 +105,7 @@ static const char *sErrorText[MUSIC_ERR_COUNT] = {
         "Invalid argument",
         "Not enough memory",
         "The file could not be read",
    -    "Not a music file (MOD, S3M, XM, IT or MIDI)",
    +    "Not a music file (MOD, S3M, XM, IT, or MIDI)",
         "The music file is damaged or incomplete",
         "No sound device can play this music",
         "The instrument bank for MIDI music is missing or damaged",
    diff --git a/src/libs/kpunch/music/music.dhs b/src/libs/kpunch/music/music.dhs
    index 620fc4db..821a9510 100644
    --- a/src/libs/kpunch/music/music.dhs
    +++ b/src/libs/kpunch/music/music.dhs
    @@ -71,7 +71,7 @@ if (rc != MUSIC_OK) {
     }
     .endcode
     
    -musicLoad and musicLoadMem tell the kind by the contents: a MIDI file (or a RIFF RMID file around one) by its header, anything else is offered to the module player. Errors: MUSIC_ERR_OPEN (unreadable file), MUSIC_ERR_FORMAT (not music DVX plays, or a type 2 MIDI file), MUSIC_ERR_CORRUPT, MUSIC_ERR_NO_MEMORY, MUSIC_ERR_NO_DEVICE (no sound device could start), MUSIC_ERR_BANK (MIDI with no usable instrument bank and no MPU-401 port). Nothing plays until musicPlay(music, loop), which plays from the current position (the start after a load or a stop; a music that played to its end starts over). musicPause / musicResume, musicStop (back to the start), musicIsPlaying, musicSetVolume (0..MUSIC_VOLUME_MAX, per music; the master volume applies too - on the FM chip and the MPU-401 port as it was when the music started or its volume was last set), musicGetPosition / musicSetPosition (milliseconds), musicGetInfo (kind, where it plays, a description of the device, title, length, position, state) and musicFree complete the set. musicSetEndFn(music, fn, userData) asks for a call when a music that does not loop plays to its end; it comes in the shell's idle time as the program that owns the music, never from an interrupt.
    +musicLoad and musicLoadMem tell the kind by the contents: a MIDI file (or a RIFF RMID file around one) by its header, anything else is offered to the module player. Errors: MUSIC_ERR_OPEN (unreadable file), MUSIC_ERR_FORMAT (not music DVX plays, or a type 2 MIDI file), MUSIC_ERR_CORRUPT, MUSIC_ERR_NO_MEMORY, MUSIC_ERR_NO_DEVICE (no sound device could start), MUSIC_ERR_BANK (MIDI with no usable instrument bank and no MPU-401 port). Nothing plays until musicPlay(music, loop), which plays from the current position (the start after a load or a stop; a music that played to its end starts over). musicPause / musicResume, musicStop (back to the start), musicIsPlaying, musicSetVolume (0..MUSIC_VOLUME_MAX, per music; the master volume applies too - on the FM chip and the MPU-401 port as it was when the music started or its volume was last set), musicGetPosition / musicSetPosition (milliseconds), musicGetInfo (kind, where it plays, a description of the device, title, length, position, state), and musicFree complete the set. musicSetEndFn(music, fn, userData) asks for a call when a music that does not loop plays to its end; it comes in the shell's idle time as the program that owns the music, never from an interrupt.
     
     Setting the position of a MIDI file sends the instruments and controllers set before that point (no notes). A module goes to the start of the pattern that holds the time.
     
    @@ -94,13 +94,13 @@ On the FM chip and the MPU-401 port the song is played by the timer service (500
     
     .h2 General MIDI on FM
     
    -Instruments come from an OP2 bank (the DMX "GENMIDI" format: 128 melodic instruments and 47 percussion instruments for notes 35 to 81, each one or two two-operator voices, with a note offset, a fixed note for percussion and a detune for the second voice). DVX ships Freedoom's GENMIDI bank (see Licences below); DVX.INI [audio] midiBank names another OP2 file instead (a path from the DVX folder), for example a bank made with an OPL editor, or another freely licensed bank such as DMXOPL. The bank is read once, at the first MIDI music that needs it.
    +Instruments come from an OP2 bank (the DMX "GENMIDI" format: 128 melodic instruments and 47 percussion instruments for notes 35 to 81, each one or two two-operator voices, with a note offset, a fixed note for percussion, and a detune for the second voice). DVX ships Freedoom's GENMIDI bank (see Licences below); DVX.INI [audio] midiBank names another OP2 file instead (a path from the DVX folder), for example a bank made with an OPL editor, or another freely licensed bank such as DMXOPL. The bank is read once, at the first MIDI music that needs it.
     
    -What is played: note on / off, velocity, program change, channel volume (CC7), expression (CC11), pan (CC10: left, both or right on an OPL3 and the software synthesizer), sustain pedal (CC64), pitch bend with the bend range from RPN 0 (default 2 semitones), all notes off (CC123, CC120), reset controllers (CC121), and General MIDI System On (a reset). Channel 10 plays percussion. Loudness is velocity x volume x expression x the music's volume on a 40 log10 curve, added as attenuation to the voice's level. OP2 banks are tuned for DMX, which sounds note n where MIDI has note n + 12, so notes are played an octave up from the bank's offsets. Not played: modulation (CC1), aftertouch, the FM chip's own rhythm mode, four-operator voices.
    +What is played: note on / off, velocity, program change, channel volume (CC7), expression (CC11), pan (CC10: left, both, or right on an OPL3 and the software synthesizer), sustain pedal (CC64), pitch bend with the bend range from RPN 0 (default 2 semitones), all notes off (CC123, CC120), reset controllers (CC121), and General MIDI System On (a reset). Channel 10 plays percussion. Loudness is velocity x volume x expression x the music's volume on a 40 log10 curve, added as attenuation to the voice's level. OP2 banks are tuned for DMX, which sounds note n where MIDI has note n + 12, so notes are played an octave up from the bank's offsets. Not played: modulation (CC1), aftertouch, the FM chip's own rhythm mode, four-operator voices.
     
     .h2 The Software Synthesizer
     
    -When there is no FM chip (or it is busy), MIDI plays on DVX's own software FM synthesizer: an integer two-operator FM chip modelled on the OPL2 (the same log-sine and exponential tables, waveforms 0 to 3, feedback, the chip's envelope rates and key scaling, key scale level, tremolo and vibrato) with 24 voices, rendering at the device's rate into a stream. It is driven with the same bank and the same register values as the chip, so music sounds alike on both. The host tests compare it with emu8950, an accurate OPL2 emulator, for several patches: pitch, loudness over time and harmonics agree closely. Cost on a 486: roughly 1 to 2 percent of a 486DX2-66 per sounding voice at 22,050 Hz.
    +When there is no FM chip (or it is busy), MIDI plays on DVX's own software FM synthesizer: an integer two-operator FM chip modelled on the OPL2 (the same log-sine and exponential tables, waveforms 0 to 3, feedback, the chip's envelope rates and key scaling, key scale level, tremolo, and vibrato) with 24 voices, rendering at the device's rate into a stream. It is driven with the same bank and the same register values as the chip, so music sounds alike on both. The host tests compare it with emu8950, an accurate OPL2 emulator, for several patches: pitch, loudness over time, and harmonics agree closely. Cost on a 486: roughly 1 to 2 percent of a 486DX2-66 per sounding voice at 22,050 Hz.
     
     .h2 The MPU-401 Port
     
    @@ -114,7 +114,7 @@ With DVX.INI [audio] midi = mpu401, MIDI goes out of the MPU-401 port (audio.lib
       raw        Anything: the music exactly as it is, nothing added (for music written for an MT-32, with its own set-up messages).
     .endtable
     
    -Stopping, pausing or freeing a music sends sustain off and all notes off on every channel the synthesizer hears.
    +Stopping, pausing, or freeing a music sends sustain off and all notes off on every channel the synthesizer hears.
     
     .h2 Live MIDI
     
    @@ -127,11 +127,11 @@ musicMidiNoteOn(keys, 0, 60, 100);             // middle C
     musicMidiNoteOff(keys, 0, 60);
     .endcode
     
    -musicMidiOpen opens a music with no file that plays at once, on the same synthesizer a MIDI file would use. musicMidiNoteOn, musicMidiNoteOff, musicMidiProgram, musicMidiControl and musicMidiPitchBend (-8192..8191) send channel messages (channels 0 to 15, data 0 to 127) to it, or to a MIDI file that is playing (for dynamic game scores); MUSIC_ERR_ARG for a module or a value out of range.
    +musicMidiOpen opens a music with no file that plays at once, on the same synthesizer a MIDI file would use. musicMidiNoteOn, musicMidiNoteOff, musicMidiProgram, musicMidiControl, and musicMidiPitchBend (-8192..8191) send channel messages (channels 0 to 15, data 0 to 127) to it, or to a MIDI file that is playing (for dynamic game scores); MUSIC_ERR_ARG for a module or a value out of range.
     
     .h2 Modules
     
    -MOD (ProTracker and compatibles), S3M, XM and IT through libxmp-lite 4.7.3 (thirdparty/libxmp-lite, unmodified; formats only, no depackers or ProWizard). Stereo separation is 50 percent (softer than the Amiga's hard left and right). The whole module is held in memory. libxmp works out pitches with floating point, so modules play in the shell's idle time, never at interrupt time; on a 486SX (no maths coprocessor) that goes through floating point emulation and is slow. Rough cost on a 486DX2-66 at 22,050 Hz: 5 percent for a 4-channel MOD, 20 to 25 percent for a 16-channel XM.
    +MOD (ProTracker and compatibles), S3M, XM, and IT through libxmp-lite 4.7.3 (thirdparty/libxmp-lite, unmodified; formats only, no depackers or ProWizard). Stereo separation is 50 percent (softer than the Amiga's hard left and right). The whole module is held in memory. libxmp works out pitches with floating point, so modules play in the shell's idle time, never at interrupt time; on a 486SX (no maths coprocessor) that goes through floating point emulation and is slow. Rough cost on a 486DX2-66 at 22,050 Hz: 5 percent for a 4-channel MOD, 20 to 25 percent for a 16-channel XM.
     
     .h2 Music Macros
     
    @@ -144,7 +144,7 @@ musicMmlPlay(tune, "MB T140 O3 L8 CDEC CDEC EFG4 EFG4");
     
     A macro queue plays tunes written in QBasic's music macro language (the commands are listed in music.h and in the BASIC language reference: notes A-G with sharps and flats, N, O, > and <, L, P, dots, T, MN / ML / MS, MF / MB). musicMmlPlay parses the whole string first; a mistake anywhere returns MUSIC_ERR_MACRO and queues nothing. The settings last from one string to the next. musicMmlTone queues a square-wave tone of milliHz thousandths of a hertz (0: silence) for us microseconds (QBasic's SOUND). musicMmlQueued counts the notes not yet played, musicMmlStop silences the queue, musicMmlBusy says whether a caller that follows QBasic should still wait (foreground mode: until everything has played; background: while more than MUSIC_MML_BACKGROUND_MAX notes wait) and moves the queue along. A queue belongs to the program that opened it and is freed when that program ends.
     
    -Timing is QBasic's: a whole note is four beats of 60 / T seconds, each dot adds half again, and a note sounds for 7/8 (MN), all (ML) or 3/4 (MS) of its length. Octave 3 starts at middle C; octave 3's A is 440 Hz. The notes play as square waves on an audio.lib note queue, each to the exact sample (on the PC speaker as tones). DVX's own I command (I1 to I128) plays the notes that follow on that General MIDI instrument: each run of such notes becomes a small MIDI file that plays like any MIDI music (on the synthesizer DVX.INI [audio] midi chooses); I0 returns to tones. Runs play one after the other; when the instruments cannot play (no instrument bank, no device) the run plays as tones.
    +Timing is QBasic's: a whole note is four beats of 60 / T seconds, each dot adds half again, and a note sounds for 7/8 (MN), all (ML), or 3/4 (MS) of its length. Octave 3 starts at middle C; octave 3's A is 440 Hz. The notes play as square waves on an audio.lib note queue, each to the exact sample (on the PC speaker as tones). DVX's own I command (I1 to I128) plays the notes that follow on that General MIDI instrument: each run of such notes becomes a small MIDI file that plays like any MIDI music (on the synthesizer DVX.INI [audio] midi chooses); I0 returns to tones. Runs play one after the other; when the instruments cannot play (no instrument bank, no device) the run plays as tones.
     
     .h2 Several Programs at Once
     
    @@ -154,7 +154,7 @@ Every music plays through one audio context of the library's own; audio.lib mixe
     
     src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName) and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums). A number only works for the program that got it. MusicGetVolume(music) (musicGetVolume in percent) and MusicInfo$(music, field) (musicGetInfo: "title", "output", "kind" = "module" / "midi" / "live" for MusicKindE, "where" = "" / "stream" / "mpu401" / "fm" / "software" for MusicOutputE, "paused" and "looping" as "-1" / "0"; any case, "" for another field). music.bas also declares the raw MIDI port calls of audio.lib (MidiPortInfo$, MidiAcquire, MidiPut, MidiRelease; see the sound library).
     
    -The BASIC runtime itself calls musicBasBeep, musicBasSound(hz, ticks), musicBasPlay(macro), musicBasPlayCount and musicBasBusy for the statements BEEP, SOUND, PLAY and the PLAY(n) function; each BASIC program has its own macro queue. QBasic's X command and "=" numbers (given with VARPTR$) are filled in by the BASIC runtime before musicBasPlay is called, so the macro language itself has neither. SOUND's duration is in clock ticks (18.2 a second) and 0 stops the queue; BEEP is 800 Hz for a quarter second.
    +The BASIC runtime itself calls musicBasBeep, musicBasSound(hz, ticks), musicBasPlay(macro), musicBasPlayCount, and musicBasBusy for the statements BEEP, SOUND, and PLAY and the PLAY(n) function; each BASIC program has its own macro queue. QBasic's X command and "=" numbers (given with VARPTR$) are filled in by the BASIC runtime before musicBasPlay is called, so the macro language itself has neither. SOUND's duration is in clock ticks (18.2 a second) and 0 stops the queue; BEEP is 800 Hz for a quarter second.
     
     .h2 Licences
     
    diff --git a/src/libs/kpunch/print/print.dhs b/src/libs/kpunch/print/print.dhs
    index c61892f6..8578c940 100644
    --- a/src/libs/kpunch/print/print.dhs
    +++ b/src/libs/kpunch/print/print.dhs
    @@ -38,7 +38,7 @@
     
     .h1 Printing Library
     
    -Printer ports and print jobs, text laid out on pages, pictures for dot-matrix and laser printers, the printer settings and the common Print and Page Setup dialogs. Notepad, Image Viewer, Icon Editor, the Control Panel's Printer page and BASIC's LPRINT all print through it; any program can.
    +Printer ports and print jobs, text laid out on pages, pictures for dot-matrix and laser printers, the printer settings, and the common Print and Page Setup dialogs. Notepad, Image Viewer, Icon Editor, the Control Panel's Printer page, and BASIC's LPRINT all print through it; any program can.
     
     Header: print/print.h
     
    @@ -46,7 +46,7 @@ Loaded as: bin/libs/kpunch/print/print.lib
     
     .h2 Settings
     
    -The printer is chosen once for everything, in DVX.INI [printer] (the Control Panel's Printer page and the Page Setup dialog edit it): where output goes (PrintDestE: LPT1, LPT2, LPT3 or a file), the printer model (PrintModelE: text only, Epson 9-pin, Epson 24-pin, HP LaserJet) and the page setup (PrintPageSetupT).
    +The printer is chosen once for everything, in DVX.INI [printer] (the Control Panel's Printer page and the Page Setup dialog edit it): where output goes (PrintDestE: LPT1, LPT2, LPT3, or a file), the printer model (PrintModelE: text only, Epson 9-pin, Epson 24-pin, HP LaserJet), and the page setup (PrintPageSetupT).
     
     .table
       Function                                   Description
    @@ -60,12 +60,12 @@ The printer is chosen once for everything, in DVX.INI [printer] (the Control Pan
       printDestChoices[] / printModelChoices[]   The labels and INI spellings (PrefsChoiceT), indexed by PrintDestE / PrintModelE.
       printDestLabel(d) / printModelLabel(m)     The label of one choice (for programs that look the library up by name).
       printDeviceProblem(dest, file)             NULL when the destination can be used, else why not (printing to a file needs its name).
    -  printDeviceStore(h, model, dest, file)     Put model, destination and file into the [printer] section of h with the page setup as it is in DVX.INI now (the caller saves h). The Control Panel's OK and print.bas use it.
    +  printDeviceStore(h, model, dest, file)     Put model, destination, and file into the [printer] section of h with the page setup as it is in DVX.INI now (the caller saves h). The Control Panel's OK and print.bas use it.
     .endtable
     
     .h2 Jobs and Printer Problems
     
    -A job sends bytes to a printer port or into a file. On DOS a port is driven through the BIOS printer service (INT 17h); a byte is sent only when a status check says the printer is ready, so the BIOS never waits by itself. While the printer is busy the job calls the idle hook; a printer busy for PRINT_BUSY_TIMEOUT_MS (10 seconds), out of paper, off line or reporting an error is a problem, and so is a file that cannot be written (disk full). A problem goes to the ask hook: Retry tries again, Cancel ends the job and every later write fails at once.
    +A job sends bytes to a printer port or into a file. On DOS a port is driven through the BIOS printer service (INT 17h); a byte is sent only when a status check says the printer is ready, so the BIOS never waits by itself. While the printer is busy the job calls the idle hook; a printer busy for PRINT_BUSY_TIMEOUT_MS (10 seconds), out of paper, off line, or reporting an error is a problem, and so is a file that cannot be written (disk full). A problem goes to the ask hook: Retry tries again, Cancel ends the job and every later write fails at once.
     
     .code
     PrintHooksT hooks = { myAsk, myIdle, NULL, myState };
    @@ -88,13 +88,13 @@ if (!job) {
       finished(ctx, closing)        When a write that called idle returns, and (closing true) when the job is freed.
     .endtable
     
    -printJobWrite returns once every byte was taken, or false when the job is cancelled. printJobBytes gives the count sent, printJobCancelled whether it was cancelled. printStatusProblem(status, afterWrite, where, out, size) turns BIOS status bits (PRINT_STATUS_*) into the message the user sees. printSetPortOps replaces the port services (present / status / write) with your own, as the tests do to play a printer that is busy or out of paper; NULL restores the real ones. Elsewhere than DOS the real ports always exist, are always ready and append to lpt1.prn .. lpt3.prn in the working directory.
    +printJobWrite returns once every byte was taken, or false when the job is cancelled. printJobBytes gives the count sent, printJobCancelled whether it was cancelled. printStatusProblem(status, afterWrite, where, out, size) turns BIOS status bits (PRINT_STATUS_*) into the message the user sees. printSetPortOps replaces the port services (present / status / write) with your own, as the tests do to play a printer that is busy or out of paper; NULL restores the real ones. Elsewhere than DOS the real ports always exist, are always ready, and append to lpt1.prn .. lpt3.prn in the working directory.
     
     .h2 Text
     
     printTextJob(job, model, text, len, &pageSetup, &info) prints plain text on pages. Lines end with LF or CR LF; a form feed in the text starts a new page. Each printed line starts with marginLeft blanks and ends with CR LF, tabs are expanded to tabWidth, other control characters print as blanks, and a line longer than the text width is wrapped at the last blank before it (a word longer than a whole line is split) or, with wrap off, cut off. Every page ends with a form feed. For the Epson and LaserJet models the printer's reset command comes first (and, for the LaserJet, last).
     
    -A page is marginTop lines with the header in their middle line, the text, then marginBottom lines with the footer in their middle line. Header and footer codes: &f file name (PrintTextInfoT.title), &p page number, &n number of pages, &d date, &t time (PrintTextInfoT.date / time, or today and now), && an ampersand; &l, &c and &r put what follows at the left, centre or right (text before any of them is centred). printFormatHeader expands a header for one page.
    +A page is marginTop lines with the header in their middle line, the text, then marginBottom lines with the footer in their middle line. Header and footer codes: &f file name (PrintTextInfoT.title), &p page number, &n number of pages, &d date, &t time (PrintTextInfoT.date / time, or today and now), && an ampersand; &l, &c, and &r put what follows at the left, centre, or right (text before any of them is centred). printFormatHeader expands a header for one page.
     
     PrintTextInfoT also chooses a page range (firstPage, lastPage; 0 = all) and the number of copies. printTextPageCount returns the number of pages text takes.
     
    @@ -120,7 +120,7 @@ A PrintDriverT holds the resolution, the printable size in dots, the band height
       printDialog(ctx, title, pageCount, &choice)           The common Print dialog: print to a port or a file, copies and, when pageCount > 0, all pages or a range. Starts from the settings; true with the PrintChoiceT filled when the user chose Print.
       printPageSetupDialog(ctx)                             The Page Setup dialog; saves the page setup to DVX.INI.
       printUiText(ctx, text, title, askFirst)               Print text with the page setup on the printer in the settings (the Print dialog first when askFirst).
    -  printUiTextChoice(ctx, text, title, &choice)          Print text on the destination, copies and page range of a PrintChoiceT (a Print dialog answer, or one the program filled) without asking; printUiText ends here.
    +  printUiTextChoice(ctx, text, title, &choice)          Print text on the destination, copies, and page range of a PrintChoiceT (a Print dialog answer, or one the program filled) without asking; printUiText ends here.
       printUiImageFile(ctx, path, title, askFirst)          Print a picture file (BMP, PNG, JPEG, GIF).
       printUiWindow(ctx, win, askFirst)                     Print a window's contents as a picture.
       printUiOpenPort(ctx, port)                            A job with the user interface hooks on LPTn (0-based) or PRINT_PORT_DEFAULT.
    @@ -130,7 +130,7 @@ Jobs opened by these ask about printer problems with a Retry / Cancel message bo
     
     .h2 BASIC
     
    -src/include/basic/print.bas declares PrintTextDialog, PrintText, PrintPictureDialog, PrintPicture, PrintPageSetup and PrintCanPrintPictures. For the printer settings it also declares PrintModelCount / PrintModelLabel$, PrintDestCount / PrintDestLabel$ / PrintDestIsFile, PrintSettingModel / PrintSettingDest / PrintSettingFile$ (printSettingsRead on a prefs.bas handle, through prefsBasHandle), PrintSettingsProblem$ (printDeviceProblem) and PrintSettingsStore (printDeviceStore).
    +src/include/basic/print.bas declares PrintTextDialog, PrintText, PrintPictureDialog, PrintPicture, PrintPageSetup, and PrintCanPrintPictures. For the printer settings it also declares PrintModelCount / PrintModelLabel$, PrintDestCount / PrintDestLabel$ / PrintDestIsFile, PrintSettingModel / PrintSettingDest / PrintSettingFile$ (printSettingsRead on a prefs.bas handle, through prefsBasHandle), PrintSettingsProblem$ (printDeviceProblem), and PrintSettingsStore (printDeviceStore).
     
     .table
       BASIC                                             C it calls
    @@ -147,4 +147,4 @@ src/include/basic/print.bas declares PrintTextDialog, PrintText, PrintPictureDia
       PrintStatusProblem$(status, afterWrite, where)    printStatusProblem ("" when the status shows none).
     .endtable
     
    -print.bas also has CONSTs for the PRINT_STATUS_ bits and the page setup limits and defaults (PRINT_LINES_STANDARD ... PRINT_COPIES_MAX); a formrt test compares them with print.h. The BASIC runtime gives the library the GUI context (printBasSetContext) when a program starts, and runs LPRINT, LPRINT USING and OPEN "LPT1:" / "PRN" through printUiOpenPort, printJobWrite and printJobClose. See the BASIC language reference (LPRINT, File I/O, Include Libraries).
    +print.bas also has CONSTs for the PRINT_STATUS_ bits and the page setup limits and defaults (PRINT_LINES_STANDARD ... PRINT_COPIES_MAX); a formrt test compares them with print.h. The BASIC runtime gives the library the GUI context (printBasSetContext) when a program starts, and runs LPRINT, LPRINT USING, and OPEN "LPT1:" / "PRN" through printUiOpenPort, printJobWrite, and printJobClose. See the BASIC language reference (LPRINT, File I/O, Include Libraries).
    diff --git a/src/libs/kpunch/richtext/Makefile b/src/libs/kpunch/richtext/Makefile
    new file mode 100644
    index 00000000..1d4869b7
    --- /dev/null
    +++ b/src/libs/kpunch/richtext/Makefile
    @@ -0,0 +1,63 @@
    +# The MIT License (MIT)
    +#
    +# Copyright (C) 2026 Scott Duensing
    +#
    +# Permission is hereby granted, free of charge, to any person obtaining a copy
    +# of this software and associated documentation files (the "Software"), to
    +# deal in the Software without restriction, including without limitation the
    +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +# sell copies of the Software, and to permit persons to whom the Software is
    +# furnished to do so, subject to the following conditions:
    +#
    +# The above copyright notice and this permission notice shall be included in
    +# all copies or substantial portions of the Software.
    +#
    +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +# IN THE SOFTWARE.
    +
    +# Styled Text Library Makefile for DJGPP cross-compilation
    +#
    +# Builds richtext.lib -- styled text (runs, styles, paragraphs) and RTF
    +# in and out, for the RichEdit widget and BASIC (richtext.bas).
    +
    +include ../../../common.mk
    +
    +CFLAGS        = $(DOS_CFLAGS) -I. -I../libdvx -I../libdvx/platform -I../libtasks -I../libdvx/thirdparty
    +
    +OBJDIR  = ../../../../obj/richtext
    +LIBSDIR = ../../../../bin/libs
    +
    +SRCS   = richText.c richRtf.c richTextBas.c
    +OBJS   = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
    +TARGETDIR = $(LIBSDIR)/kpunch/richtext
    +TARGET    = $(TARGETDIR)/richtext.lib
    +
    +.PHONY: all clean
    +
    +all: $(TARGET) $(TARGETDIR)/richtext.dep
    +
    +$(TARGETDIR)/richtext.dep: richtext.dep | $(TARGETDIR)
    +	sed 's/$$/\r/' $< > $@
    +
    +$(TARGET): $(OBJS) | $(TARGETDIR)
    +	$(DXE3GEN) -o $(TARGETDIR)/richtext.dxe -U $(OBJS)
    +	mv $(TARGETDIR)/richtext.dxe $@
    +
    +$(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR)
    +	$(CC) $(CFLAGS) -c -o $@ $<
    +
    +$(OBJDIR):
    +	mkdir -p $(OBJDIR)
    +
    +$(TARGETDIR):
    +	mkdir -p $(TARGETDIR)
    +
    +-include $(OBJS:.o=.d)
    +
    +clean:
    +	rm -rf $(OBJS) $(OBJS:.o=.d) $(TARGET) $(TARGETDIR)/richtext.dep $(TARGETDIR)
    diff --git a/src/libs/kpunch/richtext/README.md b/src/libs/kpunch/richtext/README.md
    new file mode 100644
    index 00000000..e505186e
    --- /dev/null
    +++ b/src/libs/kpunch/richtext/README.md
    @@ -0,0 +1,15 @@
    +# richtext -- Styled Text and RTF
    +
    +Styled text for DVX: text in code page 437 with paragraphs, covered by
    +runs in fonts, sizes, styles, and colors, with one alignment per
    +paragraph (RichTextT); the editing operations a word processor needs
    +(copy a range, replace a range, restyle a range, paragraph alignment);
    +and RTF in and out.  The RichEdit widget keeps its document, undo steps,
    +and clipboard in it; BASIC reaches RTF conversion through
    +src/include/basic/richtext.bas.
    +
    +## Documentation
    +
    +Full reference is in `richtext.dhs` (this directory), compiled into the
    +DVX System Reference.  Open via the Help Viewer app, or read
    +`docs/dvx_system_reference.html` after `make`.
    diff --git a/src/libs/kpunch/richtext/richRtf.c b/src/libs/kpunch/richtext/richRtf.c
    new file mode 100644
    index 00000000..5b11c851
    --- /dev/null
    +++ b/src/libs/kpunch/richtext/richRtf.c
    @@ -0,0 +1,679 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// richRtf.c  -- RTF in and out for styled text (richText.h)
    +//
    +// Writing, on one line (a line break in RTF is not text, and one line
    +// keeps the document a single string anywhere, a .frm file included): a
    +// font table (font 0 is the default family, then each other family
    +// once), a color table (entry 0 is the default color), then each
    +// paragraph as \pard and its alignment and ended by \par (the last one
    +// too, as other programs write it), each run as \plain and its whole
    +// style, and the characters: ASCII as itself, the rest as \uN with a '?'
    +// for readers without Unicode.  Reading: a group stack of character and
    +// paragraph state; text arrives in the current style, \par ends a
    +// paragraph in the alignment then in effect.  Characters come in as
    +// Windows-1252 (\'hh) or Unicode (\uN) and are stored as code page 437,
    +// '?' for those it lacks.
    +
    +#include "richText.h"
    +#include "dvxDlg.h"
    +#include "dvxFontFmt.h"
    +#include "stb_ds_wrap.h"
    +
    +#include 
    +#include 
    +#include 
    +#include 
    +#include 
    +
    +#define RTF_MAGIC          "{\\rtf"
    +#define RTF_MAGIC_LEN      5
    +#define RTF_WORD_MAX       32         // a control word's letters
    +#define RTF_NUM_MAX        12         // a control word's number, as text
    +#define RTF_HALF_POINTS    3          // \fs is half points: pixels * 3 / 2 (a pixel taken as 3/4 point)
    +#define RTF_PIXELS         2
    +#define RTF_UNICODE_WRAP   65536      // \uN is a signed 16-bit number
    +#define RTF_UC_DEFAULT     1          // characters after \uN for readers without Unicode
    +#define RTF_FALLBACK       '?'
    +#define RTF_HEX_BASE       16
    +#define RTF_DEC_BASE       10
    +#define CP1252_HIGH_FIRST  0x80       // Windows-1252 0x80-0x9F differ from Latin-1
    +#define CP1252_HIGH_COUNT  32
    +#define ASCII_PRINT_FIRST  0x20
    +#define ASCII_PRINT_LAST   0x7E
    +#define CHAR_NEWLINE       '\n'
    +
    +// Destinations
    +typedef enum {
    +    RtfDestTextE,
    +    RtfDestFontsE,
    +    RtfDestColorsE,
    +    RtfDestSkipE
    +} RtfDestE;
    +
    +// The state a group keeps
    +typedef struct {
    +    RtfDestE dest;
    +    int32_t  font;                // \f number (-1: the default)
    +    int32_t  size;                // pixels (0: the default)
    +    uint32_t style;               // FONT_STYLE_*
    +    int32_t  color;               // \cf number (0: the default)
    +    int32_t  align;               // FLOW_ALIGN_*
    +    int32_t  uc;                  // \ucN
    +} RtfStateT;
    +
    +typedef struct {
    +    int32_t num;
    +    char    name[FONT_FAMILY_MAX];
    +} RtfFontT;
    +
    +typedef struct {
    +    const char *p;
    +    const char *end;
    +    RtfStateT  *stack;            // stb_ds; the last is the current group's
    +    RtfFontT   *fonts;            // stb_ds
    +    uint32_t   *colors;           // stb_ds; [0] the default
    +    int32_t     deff;             // \deffN
    +    int32_t     skip;             // characters still to drop after \uN
    +    int32_t     defFont;          // the font table entry being read
    +    char        defName[FONT_FAMILY_MAX];
    +    int32_t     defNameLen;
    +    int32_t     red;              // the color table entry being read
    +    int32_t     green;
    +    int32_t     blue;
    +    bool        colorSet;
    +    RichTextT  *out;
    +} RtfReadT;
    +
    +// Windows-1252 0x80-0x9F as Unicode (0: none)
    +static const uint16_t sCp1252High[CP1252_HIGH_COUNT] = {
    +    0x20AC, 0x0000, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x0000, 0x017D, 0x0000,
    +    0x0000, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x0000, 0x017E, 0x0178
    +};
    +
    +// Destinations whose text is not the document's
    +static const char *const sSkipDests[] = {
    +    "author", "buptim", "colorschememapping", "comment", "company", "creatim", "datastore", "doccomm", "footer", "footerf", "footerl", "footerr",
    +    "footnote", "generator", "header", "headerf", "headerl", "headerr", "info", "keywords", "latentstyles", "listoverridetable", "listtable",
    +    "operator", "pict", "printim", "revtim", "rsidtbl", "stylesheet", "subject", "themedata", "title", "xmlnstbl", "fldinst", "object"
    +};
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +static void     addChar(RtfReadT *r, int32_t ch);
    +static void     addUnicode(RtfReadT *r, uint32_t code);
    +static void     applyWord(RtfReadT *r, const char *word, bool hasNum, int32_t num);
    +static int32_t  colorIndex(uint32_t **colors, uint32_t rgb);
    +static void     endParagraph(RtfReadT *r);
    +static int32_t  familyIndex(const char ***families, const char *family);
    +static bool     isSkipDest(const char *word);
    +static void     putEscaped(char **out, const char *text);
    +static void     putStr(char **out, const char *text);
    +static void     putWord(char **out, const char *word, int32_t num);
    +static void     readControl(RtfReadT *r);
    +static void     readFontText(RtfReadT *r, char c);
    +static RichStyleT readStyle(const RtfReadT *r);
    +bool            richRtfIs(const char *text, int32_t len);
    +bool            richRtfRead(RichTextT *rt, const char *text, int32_t len);
    +char           *richRtfWrite(const RichTextT *rt, const char *defaultFamily, int32_t *outLen);
    +static int32_t  sizeToHalfPoints(int32_t px);
    +
    +
    +// A character of the document in the current style (none in a skipped
    +// destination, or while dropping a \uN's stand-ins)
    +static void addChar(RtfReadT *r, int32_t ch) {
    +    if (arrlast(r->stack).dest != RtfDestTextE) {
    +        return;
    +    }
    +
    +    if (r->skip > 0) {
    +        r->skip--;
    +        return;
    +    }
    +
    +    RichStyleT style = readStyle(r);
    +    int32_t    index = richTextIntern(r->out, &style);
    +    int32_t    at    = richTextLength(r->out);
    +
    +    if (arrlen(r->out->runs) == 0 || arrlast(r->out->runs).style != index) {
    +        RichRunT run;
    +
    +        run.start = at;
    +        run.style = index;
    +        arrput(r->out->runs, run);
    +    }
    +
    +    arrput(r->out->text, (char)ch);
    +
    +    if (ch == CHAR_NEWLINE) {
    +        arrput(r->out->aligns, FLOW_ALIGN_LEFT);
    +    }
    +}
    +
    +
    +// A Unicode character as code page 437 ('?' when it has none)
    +static void addUnicode(RtfReadT *r, uint32_t code) {
    +    int32_t ch = (code >= ASCII_PRINT_FIRST && code <= ASCII_PRINT_LAST) ? (int32_t)code : dfnFromUnicode(code);
    +
    +    addChar(r, (ch > 0) ? ch : RTF_FALLBACK);
    +}
    +
    +
    +// Control word word (with its number, if any)
    +static void applyWord(RtfReadT *r, const char *word, bool hasNum, int32_t num) {
    +    RtfStateT *st = &arrlast(r->stack);
    +
    +    if (strcmp(word, "fonttbl") == 0) {
    +        st->dest   = RtfDestFontsE;
    +        r->defFont = -1;
    +    } else if (strcmp(word, "colortbl") == 0) {
    +        st->dest     = RtfDestColorsE;
    +        r->colorSet  = false;
    +        r->red       = 0;
    +        r->green     = 0;
    +        r->blue      = 0;
    +    } else if (isSkipDest(word)) {
    +        st->dest = RtfDestSkipE;
    +    } else if (strcmp(word, "deff") == 0) {
    +        r->deff = num;
    +    } else if (strcmp(word, "f") == 0) {
    +        if (st->dest == RtfDestFontsE) {
    +            r->defFont    = num;
    +            r->defNameLen = 0;
    +        } else {
    +            st->font = num;
    +        }
    +    } else if (strcmp(word, "fs") == 0) {
    +        st->size = hasNum ? (num * RTF_PIXELS + RTF_HALF_POINTS / 2) / RTF_HALF_POINTS : 0;
    +    } else if (strcmp(word, "b") == 0 || strcmp(word, "i") == 0 || strcmp(word, "ul") == 0 || strcmp(word, "strike") == 0) {
    +        uint32_t bit = (word[0] == 'b') ? FONT_STYLE_BOLD : (word[0] == 'i') ? FONT_STYLE_ITALIC : (word[0] == 'u') ? FONT_STYLE_UNDERLINE : FONT_STYLE_STRIKE;
    +
    +        st->style = (hasNum && num == 0) ? (st->style & ~bit) : (st->style | bit);
    +    } else if (strcmp(word, "ulnone") == 0) {
    +        st->style &= ~(uint32_t)FONT_STYLE_UNDERLINE;
    +    } else if (strcmp(word, "plain") == 0) {
    +        st->font  = -1;
    +        st->size  = 0;
    +        st->style = 0;
    +        st->color = 0;
    +    } else if (strcmp(word, "cf") == 0) {
    +        st->color = num;
    +    } else if (strcmp(word, "red") == 0 || strcmp(word, "green") == 0 || strcmp(word, "blue") == 0) {
    +        int32_t *part = (word[0] == 'r') ? &r->red : (word[0] == 'g') ? &r->green : &r->blue;
    +
    +        *part       = num;
    +        r->colorSet = true;
    +    } else if (strcmp(word, "par") == 0 || strcmp(word, "line") == 0) {
    +        endParagraph(r);
    +    } else if (strcmp(word, "pard") == 0 || strcmp(word, "ql") == 0 || strcmp(word, "qj") == 0) {
    +        // \pard: the paragraph's defaults (left-aligned); justified is
    +        // shown left-aligned
    +        st->align = FLOW_ALIGN_LEFT;
    +    } else if (strcmp(word, "qc") == 0) {
    +        st->align = FLOW_ALIGN_CENTER;
    +    } else if (strcmp(word, "qr") == 0) {
    +        st->align = FLOW_ALIGN_RIGHT;
    +    } else if (strcmp(word, "tab") == 0) {
    +        for (int32_t s = 0; s < RICH_TAB_SPACES; s++) {
    +            addChar(r, ' ');
    +        }
    +    } else if (strcmp(word, "uc") == 0) {
    +        st->uc = num;
    +    } else if (strcmp(word, "u") == 0) {
    +        addUnicode(r, (uint32_t)((num < 0) ? num + RTF_UNICODE_WRAP : num));
    +        r->skip = st->uc;
    +    } else if (strcmp(word, "emdash") == 0 || strcmp(word, "endash") == 0) {
    +        addChar(r, '-');
    +    } else if (strcmp(word, "lquote") == 0 || strcmp(word, "rquote") == 0) {
    +        addChar(r, '\'');
    +    } else if (strcmp(word, "ldblquote") == 0 || strcmp(word, "rdblquote") == 0) {
    +        addChar(r, '"');
    +    } else if (strcmp(word, "bullet") == 0) {
    +        addUnicode(r, 0x2022);
    +    }
    +}
    +
    +
    +// The color table index of rgb (from 1), added when it has none
    +static int32_t colorIndex(uint32_t **colors, uint32_t rgb) {
    +    for (int32_t i = 0; i < arrlen(*colors); i++) {
    +        if ((*colors)[i] == rgb) {
    +            return i + 1;
    +        }
    +    }
    +
    +    arrput(*colors, rgb);
    +    return (int32_t)arrlen(*colors);
    +}
    +
    +
    +// The paragraph ends in the alignment in effect
    +static void endParagraph(RtfReadT *r) {
    +    if (arrlast(r->stack).dest != RtfDestTextE) {
    +        return;
    +    }
    +
    +    arrlast(r->out->aligns) = arrlast(r->stack).align;
    +    r->skip                 = 0;
    +    addChar(r, CHAR_NEWLINE);
    +}
    +
    +
    +// The index of family in families (font table numbers from 1; 0 is the
    +// default family), added when it has none
    +static int32_t familyIndex(const char ***families, const char *family) {
    +    for (int32_t i = 0; i < arrlen(*families); i++) {
    +        if (strcmp((*families)[i], family) == 0) {
    +            return i + 1;
    +        }
    +    }
    +
    +    arrput(*families, family);
    +    return (int32_t)arrlen(*families);
    +}
    +
    +
    +static bool isSkipDest(const char *word) {
    +    for (int32_t i = 0; i < DVX_ARRAY_LEN(sSkipDests); i++) {
    +        if (strcmp(word, sSkipDests[i]) == 0) {
    +            return true;
    +        }
    +    }
    +
    +    return false;
    +}
    +
    +
    +// text with RTF's special characters escaped (names in the font table)
    +static void putEscaped(char **out, const char *text) {
    +    for (const char *p = text; *p; p++) {
    +        if (*p == '\\' || *p == '{' || *p == '}' || *p == ';') {
    +            arrput(*out, '\\');
    +        }
    +
    +        arrput(*out, *p);
    +    }
    +}
    +
    +
    +static void putStr(char **out, const char *text) {
    +    for (const char *p = text; *p; p++) {
    +        arrput(*out, *p);
    +    }
    +}
    +
    +
    +// \word, \wordN for num >= 0
    +static void putWord(char **out, const char *word, int32_t num) {
    +    char buf[RTF_WORD_MAX + RTF_NUM_MAX];
    +
    +    if (num >= 0) {
    +        snprintf(buf, sizeof(buf), "\\%s%ld", word, (long)num);
    +    } else {
    +        snprintf(buf, sizeof(buf), "\\%s", word);
    +    }
    +
    +    putStr(out, buf);
    +}
    +
    +
    +// A control word or symbol after its backslash
    +static void readControl(RtfReadT *r) {
    +    char    word[RTF_WORD_MAX];
    +    int32_t len = 0;
    +
    +    if (r->p >= r->end) {
    +        return;
    +    }
    +
    +    // Control symbols
    +    if (!isalpha((unsigned char)*r->p)) {
    +        char c = *r->p++;
    +
    +        if (c == '\'' && r->end - r->p >= 2) {
    +            const char hex[3] = { r->p[0], r->p[1], '\0' };
    +            int32_t    b      = (int32_t)strtol(hex, NULL, RTF_HEX_BASE);
    +            uint32_t   code   = (b >= CP1252_HIGH_FIRST && b < CP1252_HIGH_FIRST + CP1252_HIGH_COUNT) ? sCp1252High[b - CP1252_HIGH_FIRST] : (uint32_t)b;
    +
    +            r->p += 2;
    +
    +            if (arrlast(r->stack).dest == RtfDestFontsE) {
    +                readFontText(r, (char)b);
    +            } else {
    +                addUnicode(r, code);
    +            }
    +        } else if (c == '*') {
    +            arrlast(r->stack).dest = RtfDestSkipE;
    +        } else if (c == '~') {
    +            addUnicode(r, dfnCp437[DFN_CHAR_COUNT - 1]);
    +        } else if (c == '_') {
    +            addChar(r, '-');
    +        } else if (c == '\n' || c == '\r') {
    +            endParagraph(r);
    +        } else if (c == '\\' || c == '{' || c == '}') {
    +            if (arrlast(r->stack).dest == RtfDestFontsE) {
    +                readFontText(r, c);
    +            } else {
    +                addChar(r, c);
    +            }
    +        }
    +
    +        return;
    +    }
    +
    +    while (r->p < r->end && isalpha((unsigned char)*r->p)) {
    +        if (len < RTF_WORD_MAX - 1) {
    +            word[len++] = *r->p;
    +        }
    +
    +        r->p++;
    +    }
    +
    +    word[len] = '\0';
    +
    +    bool    hasNum = false;
    +    bool    neg    = false;
    +    int32_t num    = 0;
    +
    +    if (r->p < r->end && *r->p == '-') {
    +        neg = true;
    +        r->p++;
    +    }
    +
    +    while (r->p < r->end && isdigit((unsigned char)*r->p)) {
    +        hasNum = true;
    +        num    = num * RTF_DEC_BASE + (*r->p - '0');
    +        r->p++;
    +    }
    +
    +    // One space ends a control word and is not text
    +    if (r->p < r->end && *r->p == ' ') {
    +        r->p++;
    +    }
    +
    +    applyWord(r, word, hasNum, neg ? -num : num);
    +}
    +
    +
    +// A character of a font table entry's name; ';' ends the entry
    +static void readFontText(RtfReadT *r, char c) {
    +    if (c == ';') {
    +        if (r->defFont >= 0) {
    +            RtfFontT f;
    +
    +            f.num = r->defFont;
    +            memcpy(f.name, r->defName, sizeof(f.name));
    +            f.name[DVX_MIN(r->defNameLen, FONT_FAMILY_MAX - 1)] = '\0';
    +            arrput(r->fonts, f);
    +        }
    +
    +        r->defFont    = -1;
    +        r->defNameLen = 0;
    +    } else if (r->defNameLen < FONT_FAMILY_MAX - 1 && (r->defNameLen > 0 || c != ' ')) {
    +        r->defName[r->defNameLen++] = c;
    +    }
    +}
    +
    +
    +// The style text is in now: the font table's family (the default for
    +// \deff and an unknown font), the size, the style bits and the color
    +static RichStyleT readStyle(const RtfReadT *r) {
    +    const RtfStateT *st = &arrlast(r->stack);
    +    RichStyleT       s;
    +
    +    memset(&s, 0, sizeof(s));
    +    s.size  = st->size;
    +    s.style = st->style;
    +    s.color = (st->color > 0 && st->color < arrlen(r->colors)) ? r->colors[st->color] : RICH_COLOR_DEFAULT;
    +
    +    for (int32_t i = 0; st->font >= 0 && st->font != r->deff && i < arrlen(r->fonts); i++) {
    +        if (r->fonts[i].num == st->font) {
    +            memcpy(s.family, r->fonts[i].name, sizeof(s.family));
    +        }
    +    }
    +
    +    return s;
    +}
    +
    +
    +bool richRtfIs(const char *text, int32_t len) {
    +    return text && len >= RTF_MAGIC_LEN && strncmp(text, RTF_MAGIC, RTF_MAGIC_LEN) == 0;
    +}
    +
    +
    +bool richRtfRead(RichTextT *rt, const char *text, int32_t len) {
    +    RtfReadT  r;
    +    RtfStateT top;
    +    RichTextT out;
    +
    +    if (!richRtfIs(text, len)) {
    +        return false;
    +    }
    +
    +    memset(&r, 0, sizeof(r));
    +    memset(&top, 0, sizeof(top));
    +    richTextInit(&out);
    +    arrsetlen(out.runs, 0);
    +    top.dest  = RtfDestTextE;
    +    top.font  = -1;
    +    top.uc    = RTF_UC_DEFAULT;
    +    top.align = FLOW_ALIGN_LEFT;
    +    r.p       = text;
    +    r.end     = text + len;
    +    r.out     = &out;
    +    r.deff    = -1;
    +    r.defFont = -1;
    +    arrput(r.stack, top);
    +
    +    while (r.p < r.end) {
    +        char c = *r.p++;
    +
    +        if (c == '{') {
    +            RtfStateT copy = arrlast(r.stack);
    +
    +            arrput(r.stack, copy);
    +        } else if (c == '}') {
    +            if (arrlen(r.stack) > 1) {
    +                RtfDestE dest = arrlast(r.stack).dest;
    +
    +                (void)arrpop(r.stack);
    +
    +                // Leaving the font table ends an entry left open
    +                if (dest == RtfDestFontsE && arrlast(r.stack).dest != RtfDestFontsE) {
    +                    r.defFont = -1;
    +                }
    +            }
    +        } else if (c == '\\') {
    +            readControl(&r);
    +        } else if (c == '\r' || c == '\n') {
    +            continue;
    +        } else if (arrlast(r.stack).dest == RtfDestFontsE) {
    +            readFontText(&r, c);
    +        } else if (arrlast(r.stack).dest == RtfDestColorsE) {
    +            if (c == ';') {
    +                arrput(r.colors, r.colorSet ? DVX_RGB(r.red, r.green, r.blue) : RICH_COLOR_DEFAULT);
    +                r.colorSet = false;
    +                r.red      = 0;
    +                r.green    = 0;
    +                r.blue     = 0;
    +            }
    +        } else {
    +            addChar(&r, (unsigned char)c);
    +        }
    +    }
    +
    +    // The last paragraph's alignment
    +    arrlast(out.aligns) = arrlast(r.stack).align;
    +
    +    if (arrlen(out.runs) == 0) {
    +        RichRunT run;
    +
    +        run.start = 0;
    +        run.style = 0;
    +        arrput(out.runs, run);
    +    }
    +
    +    // The \par that ends the last paragraph starts no paragraph
    +    if (richTextLength(&out) > 0 && arrlast(out.text) == CHAR_NEWLINE) {
    +        (void)arrpop(out.text);
    +        (void)arrpop(out.aligns);
    +
    +        while (arrlen(out.runs) > 1 && arrlast(out.runs).start >= richTextLength(&out)) {
    +            (void)arrpop(out.runs);
    +        }
    +    }
    +
    +    arrfree(r.stack);
    +    arrfree(r.fonts);
    +    arrfree(r.colors);
    +    richTextFree(rt);
    +    *rt = out;
    +    return true;
    +}
    +
    +
    +char *richRtfWrite(const RichTextT *rt, const char *defaultFamily, int32_t *outLen) {
    +    char        *buf      = NULL;      // stb_ds
    +    const char **families = NULL;      // stb_ds; font table entries from 1
    +    uint32_t    *colors   = NULL;      // stb_ds; color table entries from 1
    +    int32_t      len      = richTextLength(rt);
    +    int32_t      para     = 0;
    +
    +    // The tables: every family and color the runs use
    +    for (int32_t i = 0; i < arrlen(rt->runs); i++) {
    +        const RichStyleT *s = &rt->styles[rt->runs[i].style];
    +
    +        if (s->family[0]) {
    +            familyIndex(&families, s->family);
    +        }
    +
    +        if (s->color != RICH_COLOR_DEFAULT) {
    +            colorIndex(&colors, s->color);
    +        }
    +    }
    +
    +    putStr(&buf, "{\\rtf1\\ansi\\ansicpg1252\\deff0\\uc1{\\fonttbl{\\f0 ");
    +    putEscaped(&buf, defaultFamily ? defaultFamily : "");
    +    putStr(&buf, ";}");
    +
    +    for (int32_t i = 0; i < arrlen(families); i++) {
    +        putStr(&buf, "{");
    +        putWord(&buf, "f", i + 1);
    +        putStr(&buf, " ");
    +        putEscaped(&buf, families[i]);
    +        putStr(&buf, ";}");
    +    }
    +
    +    putStr(&buf, "}{\\colortbl;");
    +
    +    for (int32_t i = 0; i < arrlen(colors); i++) {
    +        putWord(&buf, "red", (int32_t)DVX_RGB_RED(colors[i]));
    +        putWord(&buf, "green", (int32_t)DVX_RGB_GREEN(colors[i]));
    +        putWord(&buf, "blue", (int32_t)DVX_RGB_BLUE(colors[i]));
    +        putStr(&buf, ";");
    +    }
    +
    +    putStr(&buf, "}");
    +
    +    for (int32_t at = 0, run = 0; at <= len; at++) {
    +        // A paragraph starts: its alignment
    +        if (at == 0 || rt->text[at - 1] == CHAR_NEWLINE) {
    +            int32_t align = richTextAlign(rt, para);
    +
    +            putStr(&buf, "\\pard");
    +            putStr(&buf, (align == FLOW_ALIGN_CENTER) ? "\\qc" : (align == FLOW_ALIGN_RIGHT) ? "\\qr" : "\\ql");
    +            putStr(&buf, " ");
    +            para++;
    +        }
    +
    +        if (at == len) {
    +            putStr(&buf, "\\par");
    +            break;
    +        }
    +
    +        // A run starts: its whole style
    +        if (run < arrlen(rt->runs) && rt->runs[run].start == at) {
    +            const RichStyleT *s = &rt->styles[rt->runs[run].style];
    +
    +            putStr(&buf, "\\plain");
    +            putWord(&buf, "f", s->family[0] ? familyIndex(&families, s->family) : 0);
    +
    +            if (s->size > 0) {
    +                putWord(&buf, "fs", sizeToHalfPoints(s->size));
    +            }
    +
    +            putStr(&buf, (s->style & FONT_STYLE_BOLD) ? "\\b" : "");
    +            putStr(&buf, (s->style & FONT_STYLE_ITALIC) ? "\\i" : "");
    +            putStr(&buf, (s->style & FONT_STYLE_UNDERLINE) ? "\\ul" : "");
    +            putStr(&buf, (s->style & FONT_STYLE_STRIKE) ? "\\strike" : "");
    +
    +            if (s->color != RICH_COLOR_DEFAULT) {
    +                putWord(&buf, "cf", colorIndex(&colors, s->color));
    +            }
    +
    +            putStr(&buf, " ");
    +            run++;
    +        }
    +
    +        unsigned char c = (unsigned char)rt->text[at];
    +
    +        if (c == CHAR_NEWLINE) {
    +            putStr(&buf, "\\par ");
    +        } else if (c == '\\' || c == '{' || c == '}') {
    +            arrput(buf, '\\');
    +            arrput(buf, (char)c);
    +        } else if (c >= ASCII_PRINT_FIRST && c <= ASCII_PRINT_LAST) {
    +            arrput(buf, (char)c);
    +        } else {
    +            putWord(&buf, "u", (int32_t)dfnCp437[c]);
    +            arrput(buf, RTF_FALLBACK);
    +        }
    +    }
    +
    +    putStr(&buf, "}");
    +
    +    int32_t size = (int32_t)arrlen(buf);
    +    char   *out  = (char *)malloc((size_t)size + 1);
    +
    +    if (out) {
    +        memcpy(out, buf, (size_t)size);
    +        out[size] = '\0';
    +    }
    +
    +    if (outLen) {
    +        *outLen = out ? size : 0;
    +    }
    +
    +    arrfree(buf);
    +    arrfree(families);
    +    arrfree(colors);
    +    return out;
    +}
    +
    +
    +// Pixels as RTF half points, rounded (richRtfRead's \fs rounds back)
    +static int32_t sizeToHalfPoints(int32_t px) {
    +    return (px * RTF_HALF_POINTS + 1) / RTF_PIXELS;
    +}
    diff --git a/src/libs/kpunch/richtext/richText.c b/src/libs/kpunch/richtext/richText.c
    new file mode 100644
    index 00000000..ea189f32
    --- /dev/null
    +++ b/src/libs/kpunch/richtext/richText.c
    @@ -0,0 +1,420 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// richText.c  -- styled text (richText.h): runs, styles, paragraphs
    +//
    +// Every change keeps the runs tidy: none covers nothing (but the one of
    +// an empty text), no two neighbors share a style.  Styles are interned,
    +// so one style is one index and a style compare is an index compare.
    +
    +#include "richText.h"
    +#include "stb_ds_wrap.h"
    +
    +#include 
    +
    +// ABI stamp checked by the loader (see DVX_ABI_VERSION).
    +DVX_ABI_STAMP(richtext);
    +
    +#define CHAR_NEWLINE '\n'
    +#define CHAR_RETURN  '\r'
    +#define CHAR_TAB     '\t'
    +#define CHAR_SPACE   ' '
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +static int32_t clampOffset(const RichTextT *rt, int32_t offset);
    +static void    joinRuns(RichTextT *rt);
    +static int32_t newlines(const char *text, int32_t from, int32_t to);
    +RichStyleT     richStyleChanged(const RichStyleT *style, const RichChangeT *change);
    +int32_t        richTextAlign(const RichTextT *rt, int32_t para);
    +void           richTextClear(RichTextT *rt);
    +void           richTextCopy(const RichTextT *src, int32_t from, int32_t to, RichTextT *dst);
    +void           richTextFree(RichTextT *rt);
    +void           richTextFromPlain(RichTextT *rt, const char *text, int32_t len, const RichStyleT *style);
    +void           richTextInit(RichTextT *rt);
    +int32_t        richTextInsertPlain(RichTextT *rt, int32_t at, const char *text, int32_t len, int32_t style);
    +int32_t        richTextIntern(RichTextT *rt, const RichStyleT *style);
    +int32_t        richTextLength(const RichTextT *rt);
    +int32_t        richTextParaCount(const RichTextT *rt);
    +int32_t        richTextParaEnd(const RichTextT *rt, int32_t para);
    +int32_t        richTextParaOf(const RichTextT *rt, int32_t offset);
    +int32_t        richTextParaStart(const RichTextT *rt, int32_t para);
    +int32_t        richTextReplace(RichTextT *rt, int32_t from, int32_t to, const RichTextT *what);
    +void           richTextRestyle(RichTextT *rt, int32_t from, int32_t to, const RichChangeT *change);
    +int32_t        richTextRunAt(const RichTextT *rt, int32_t offset);
    +void           richTextSetAlign(RichTextT *rt, int32_t first, int32_t last, int32_t align);
    +int32_t        richTextStyleAt(const RichTextT *rt, int32_t offset);
    +static int32_t splitRun(RichTextT *rt, int32_t offset);
    +
    +
    +static int32_t clampOffset(const RichTextT *rt, int32_t offset) {
    +    return DVX_MAX(0, DVX_MIN(offset, richTextLength(rt)));
    +}
    +
    +
    +// Runs that cover nothing go (one stays when the text is empty: its
    +// style is the text's) and neighbors in one style become one run
    +static void joinRuns(RichTextT *rt) {
    +    int32_t len = richTextLength(rt);
    +    int32_t out = 0;
    +
    +    for (int32_t i = 0; i < arrlen(rt->runs); i++) {
    +        RichRunT run  = rt->runs[i];
    +        int32_t  next = (i + 1 < arrlen(rt->runs)) ? rt->runs[i + 1].start : len;
    +        bool     last = (i + 1 == arrlen(rt->runs));
    +
    +        // Empty: dropped, unless it is the last and nothing is kept yet
    +        if (next <= run.start && !(last && out == 0)) {
    +            continue;
    +        }
    +
    +        if (out > 0 && rt->runs[out - 1].style == run.style) {
    +            continue;
    +        }
    +
    +        rt->runs[out] = run;
    +        out++;
    +    }
    +
    +    arrsetlen(rt->runs, out);
    +    rt->runs[0].start = 0;
    +}
    +
    +
    +static int32_t newlines(const char *text, int32_t from, int32_t to) {
    +    int32_t count = 0;
    +
    +    for (int32_t i = from; i < to; i++) {
    +        count += (text[i] == CHAR_NEWLINE) ? 1 : 0;
    +    }
    +
    +    return count;
    +}
    +
    +
    +RichStyleT richStyleChanged(const RichStyleT *style, const RichChangeT *change) {
    +    RichStyleT out = *style;
    +
    +    if (change->set & RICH_SET_FAMILY) {
    +        memcpy(out.family, change->to.family, sizeof(out.family));
    +    }
    +
    +    if (change->set & RICH_SET_SIZE) {
    +        out.size = change->to.size;
    +    }
    +
    +    if (change->set & RICH_SET_COLOR) {
    +        out.color = change->to.color;
    +    }
    +
    +    out.style = (out.style & ~change->styleMask) | (change->to.style & change->styleMask);
    +    return out;
    +}
    +
    +
    +int32_t richTextAlign(const RichTextT *rt, int32_t para) {
    +    return (para >= 0 && para < arrlen(rt->aligns)) ? rt->aligns[para] : FLOW_ALIGN_LEFT;
    +}
    +
    +
    +void richTextClear(RichTextT *rt) {
    +    RichStyleT def;
    +    RichRunT   run;
    +
    +    memset(&def, 0, sizeof(def));
    +    def.color = RICH_COLOR_DEFAULT;
    +    run.start = 0;
    +    run.style = 0;
    +    arrsetlen(rt->text, 0);
    +    arrsetlen(rt->runs, 0);
    +    arrsetlen(rt->styles, 0);
    +    arrsetlen(rt->aligns, 0);
    +    arrput(rt->styles, def);
    +    arrput(rt->runs, run);
    +    arrput(rt->aligns, FLOW_ALIGN_LEFT);
    +}
    +
    +
    +void richTextCopy(const RichTextT *src, int32_t from, int32_t to, RichTextT *dst) {
    +    from = clampOffset(src, from);
    +    to   = DVX_MAX(clampOffset(src, to), from);
    +    richTextClear(dst);
    +    arrsetlen(dst->runs, 0);
    +    arrsetlen(dst->aligns, 0);
    +    arrsetlen(dst->text, to - from);
    +
    +    if (to > from) {
    +        memcpy(dst->text, src->text + from, (size_t)(to - from));
    +    }
    +
    +    // The runs over the range (an empty range: the style at from)
    +    int32_t firstRun = richTextRunAt(src, from);
    +
    +    for (int32_t i = firstRun; i < arrlen(src->runs) && (src->runs[i].start < to || i == firstRun); i++) {
    +        RichRunT run;
    +
    +        run.start = DVX_MAX(src->runs[i].start, from) - from;
    +        run.style = richTextIntern(dst, &src->styles[src->runs[i].style]);
    +        arrput(dst->runs, run);
    +    }
    +
    +    // The paragraphs it touches
    +    int32_t first = richTextParaOf(src, from);
    +    int32_t count = newlines(src->text, from, to) + 1;
    +
    +    for (int32_t p = 0; p < count; p++) {
    +        arrput(dst->aligns, richTextAlign(src, first + p));
    +    }
    +
    +    joinRuns(dst);
    +}
    +
    +
    +void richTextFree(RichTextT *rt) {
    +    arrfree(rt->text);
    +    arrfree(rt->runs);
    +    arrfree(rt->styles);
    +    arrfree(rt->aligns);
    +}
    +
    +
    +void richTextFromPlain(RichTextT *rt, const char *text, int32_t len, const RichStyleT *style) {
    +    richTextClear(rt);
    +    rt->styles[0] = *style;
    +    richTextInsertPlain(rt, 0, text, len, 0);
    +}
    +
    +
    +void richTextInit(RichTextT *rt) {
    +    memset(rt, 0, sizeof(*rt));
    +    richTextClear(rt);
    +}
    +
    +
    +int32_t richTextInsertPlain(RichTextT *rt, int32_t at, const char *text, int32_t len, int32_t style) {
    +    RichTextT piece;
    +    int32_t   align = richTextAlign(rt, richTextParaOf(rt, clampOffset(rt, at)));
    +
    +    richTextInit(&piece);
    +    piece.styles[0]  = rt->styles[style];
    +    piece.aligns[0]  = align;
    +
    +    for (int32_t i = 0; text && i < len; i++) {
    +        if (text[i] == CHAR_TAB) {
    +            for (int32_t s = 0; s < RICH_TAB_SPACES; s++) {
    +                arrput(piece.text, CHAR_SPACE);
    +            }
    +        } else if (text[i] == CHAR_NEWLINE) {
    +            arrput(piece.text, CHAR_NEWLINE);
    +            arrput(piece.aligns, align);
    +        } else if (text[i] != CHAR_RETURN && text[i] != '\0') {
    +            arrput(piece.text, text[i]);
    +        }
    +    }
    +
    +    int32_t added = richTextReplace(rt, at, at, &piece);
    +
    +    richTextFree(&piece);
    +    return added;
    +}
    +
    +
    +int32_t richTextIntern(RichTextT *rt, const RichStyleT *style) {
    +    for (int32_t i = 0; i < arrlen(rt->styles); i++) {
    +        const RichStyleT *s = &rt->styles[i];
    +
    +        if (s->size == style->size && s->style == style->style && s->color == style->color && strcmp(s->family, style->family) == 0) {
    +            return i;
    +        }
    +    }
    +
    +    RichStyleT copy = *style;
    +
    +    copy.family[sizeof(copy.family) - 1] = '\0';
    +    arrput(rt->styles, copy);
    +    return (int32_t)arrlen(rt->styles) - 1;
    +}
    +
    +
    +int32_t richTextLength(const RichTextT *rt) {
    +    return (int32_t)arrlen(rt->text);
    +}
    +
    +
    +int32_t richTextParaCount(const RichTextT *rt) {
    +    return (int32_t)arrlen(rt->aligns);
    +}
    +
    +
    +int32_t richTextParaEnd(const RichTextT *rt, int32_t para) {
    +    int32_t len = richTextLength(rt);
    +    int32_t at  = richTextParaStart(rt, para);
    +
    +    while (at < len && rt->text[at] != CHAR_NEWLINE) {
    +        at++;
    +    }
    +
    +    return at;
    +}
    +
    +
    +int32_t richTextParaOf(const RichTextT *rt, int32_t offset) {
    +    return newlines(rt->text, 0, clampOffset(rt, offset));
    +}
    +
    +
    +int32_t richTextParaStart(const RichTextT *rt, int32_t para) {
    +    int32_t len = richTextLength(rt);
    +    int32_t at  = 0;
    +
    +    for (int32_t p = 0; p < para && at < len; at++) {
    +        p += (rt->text[at] == CHAR_NEWLINE) ? 1 : 0;
    +    }
    +
    +    return at;
    +}
    +
    +
    +int32_t richTextReplace(RichTextT *rt, int32_t from, int32_t to, const RichTextT *what) {
    +    from = clampOffset(rt, from);
    +    to   = DVX_MAX(clampOffset(rt, to), from);
    +
    +    int32_t para  = richTextParaOf(rt, from);
    +    int32_t gone  = to - from;
    +    int32_t added = what ? richTextLength(what) : 0;
    +
    +    // Out: the runs over the range, the text, the joined paragraphs'
    +    // alignments
    +    int32_t first = splitRun(rt, from);
    +    int32_t last  = splitRun(rt, to);
    +
    +    arrdeln(rt->runs, first, last - first);
    +
    +    for (int32_t i = first; i < arrlen(rt->runs); i++) {
    +        rt->runs[i].start -= gone;
    +    }
    +
    +    // (An empty text has no array to delete from)
    +    if (gone > 0) {
    +        arrdeln(rt->aligns, para + 1, newlines(rt->text, from, to));
    +        arrdeln(rt->text, from, gone);
    +    }
    +
    +    // In: what's text, its runs (in rt's styles) and its new paragraphs'
    +    // alignments
    +    if (added > 0) {
    +        int32_t at = splitRun(rt, from);
    +
    +        for (int32_t i = at; i < arrlen(rt->runs); i++) {
    +            rt->runs[i].start += added;
    +        }
    +
    +        for (int32_t i = 0; i < arrlen(what->runs); i++) {
    +            RichRunT run;
    +
    +            run.start = what->runs[i].start + from;
    +            run.style = richTextIntern(rt, &what->styles[what->runs[i].style]);
    +            arrins(rt->runs, at + i, run);
    +        }
    +
    +        arrinsn(rt->text, from, added);
    +        memcpy(rt->text + from, what->text, (size_t)added);
    +
    +        for (int32_t p = 1; p < arrlen(what->aligns); p++) {
    +            arrins(rt->aligns, para + p, what->aligns[p]);
    +        }
    +    }
    +
    +    joinRuns(rt);
    +    return added;
    +}
    +
    +
    +void richTextRestyle(RichTextT *rt, int32_t from, int32_t to, const RichChangeT *change) {
    +    from = clampOffset(rt, from);
    +    to   = DVX_MAX(clampOffset(rt, to), from);
    +
    +    if (to == from) {
    +        return;
    +    }
    +
    +    int32_t first = splitRun(rt, from);
    +    int32_t last  = splitRun(rt, to);
    +
    +    for (int32_t i = first; i < last; i++) {
    +        RichStyleT changed = richStyleChanged(&rt->styles[rt->runs[i].style], change);
    +
    +        rt->runs[i].style = richTextIntern(rt, &changed);
    +    }
    +
    +    joinRuns(rt);
    +}
    +
    +
    +int32_t richTextRunAt(const RichTextT *rt, int32_t offset) {
    +    int32_t lo = 0;
    +    int32_t hi = (int32_t)arrlen(rt->runs) - 1;
    +
    +    // The last run starting at or before offset
    +    while (lo < hi) {
    +        int32_t mid = (lo + hi + 1) / 2;
    +
    +        if (rt->runs[mid].start <= offset) {
    +            lo = mid;
    +        } else {
    +            hi = mid - 1;
    +        }
    +    }
    +
    +    return lo;
    +}
    +
    +
    +void richTextSetAlign(RichTextT *rt, int32_t first, int32_t last, int32_t align) {
    +    for (int32_t p = DVX_MAX(first, 0); p <= last && p < arrlen(rt->aligns); p++) {
    +        rt->aligns[p] = align;
    +    }
    +}
    +
    +
    +int32_t richTextStyleAt(const RichTextT *rt, int32_t offset) {
    +    return rt->runs[richTextRunAt(rt, offset)].style;
    +}
    +
    +
    +// The index of the run starting at offset, the run there split in two
    +// when it starts before it
    +static int32_t splitRun(RichTextT *rt, int32_t offset) {
    +    int32_t  i = richTextRunAt(rt, offset);
    +    RichRunT run;
    +
    +    if (rt->runs[i].start == offset) {
    +        return i;
    +    }
    +
    +    run.start = offset;
    +    run.style = rt->runs[i].style;
    +    arrins(rt->runs, i + 1, run);
    +    return i + 1;
    +}
    diff --git a/src/libs/kpunch/richtext/richText.h b/src/libs/kpunch/richtext/richText.h
    new file mode 100644
    index 00000000..eb09372c
    --- /dev/null
    +++ b/src/libs/kpunch/richtext/richText.h
    @@ -0,0 +1,166 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// richText.h  -- styled text, and RTF in and out
    +//
    +// A RichTextT is text in code page 437 with '\n' between paragraphs,
    +// covered by runs (each from an offset to the next run's, in a style:
    +// font family, pixel size, bold / italic / underline / strike-through,
    +// and color) and one alignment per paragraph.  A style's family "" and size 0
    +// mean the default font (the RichEdit control's own), its color
    +// RICH_COLOR_DEFAULT the default color (the control's ForeColor).  The
    +// same type holds a document, a piece of one (undo, the clipboard) and
    +// what RTF reads and writes.  Offsets number the characters from 0; the
    +// '\n' between two paragraphs is one character.  The arrays are stb_ds
    +// arrays; running out of memory ends the program as stb_ds does.
    +
    +#ifndef RICHTEXT_H
    +#define RICHTEXT_H
    +
    +#include "dvxTypes.h"
    +#include "dvxFlow.h"
    +
    +#include 
    +#include 
    +
    +// A run's color: 0x00RRGGBB, or this for the default color
    +#define RICH_COLOR_DEFAULT FLOW_COLOR_DEFAULT
    +
    +// The spaces a tab becomes (plain text and RTF bring tabs; DVX text has
    +// none)
    +#define RICH_TAB_SPACES    4
    +
    +// What richTextRestyle changes (RichChangeT.set); style bits are changed
    +// by RichChangeT.styleMask
    +#define RICH_SET_FAMILY    0x0001
    +#define RICH_SET_SIZE      0x0002
    +#define RICH_SET_COLOR     0x0004
    +
    +typedef struct {
    +    char     family[FONT_FAMILY_MAX];   // "" = the default family
    +    int32_t  size;                      // pixels; 0 = the default size
    +    uint32_t style;                     // FONT_STYLE_*
    +    uint32_t color;                     // 0x00RRGGBB or RICH_COLOR_DEFAULT
    +} RichStyleT;
    +
    +typedef struct {
    +    int32_t start;                      // the first character's offset
    +    int32_t style;                      // index in RichTextT.styles
    +} RichRunT;
    +
    +typedef struct {
    +    char       *text;                   // stb_ds; no NUL
    +    RichRunT   *runs;                   // stb_ds; the first starts at 0, each later one further on; never empty
    +    RichStyleT *styles;                 // stb_ds; [0] is the default style
    +    int32_t    *aligns;                 // stb_ds; FLOW_ALIGN_* per paragraph
    +} RichTextT;
    +
    +// A change to styles: the parts named in set and the style bits in
    +// styleMask take to's
    +typedef struct {
    +    uint32_t   set;                     // RICH_SET_*
    +    uint32_t   styleMask;               // FONT_STYLE_* bits to make to.style's
    +    RichStyleT to;
    +} RichChangeT;
    +
    +// ============================================================
    +// Styled text (richText.c)
    +// ============================================================
    +
    +// An empty text: one empty paragraph, left-aligned, in the default style
    +// (Init a new RichTextT, Clear one in use); Free releases its arrays.
    +void richTextClear(RichTextT *rt);
    +void richTextFree(RichTextT *rt);
    +void richTextInit(RichTextT *rt);
    +
    +// Its characters (the last offset).
    +int32_t richTextLength(const RichTextT *rt);
    +
    +// The index of style in rt's styles, added when it has none like it.
    +int32_t richTextIntern(RichTextT *rt, const RichStyleT *style);
    +
    +// The run the character at offset is in; at the end, the last run.
    +int32_t richTextRunAt(const RichTextT *rt, int32_t offset);
    +
    +// The style of the character at offset (richTextRunAt's).
    +int32_t richTextStyleAt(const RichTextT *rt, int32_t offset);
    +
    +// Paragraphs: one's alignment; how many; where one ends (its '\n', or the
    +// text's end); the one offset is in (its '\n' belongs to it); where one
    +// starts.
    +int32_t richTextAlign(const RichTextT *rt, int32_t para);
    +int32_t richTextParaCount(const RichTextT *rt);
    +int32_t richTextParaEnd(const RichTextT *rt, int32_t para);
    +int32_t richTextParaOf(const RichTextT *rt, int32_t offset);
    +int32_t richTextParaStart(const RichTextT *rt, int32_t para);
    +
    +// Make paragraphs first to last aligned align (FLOW_ALIGN_*).
    +void richTextSetAlign(RichTextT *rt, int32_t first, int32_t last, int32_t align);
    +
    +// The text from offset from to to (clamped) as dst (emptied first): its
    +// characters, runs, the styles they use, and the alignments of the
    +// paragraphs it touches.
    +void richTextCopy(const RichTextT *src, int32_t from, int32_t to, RichTextT *dst);
    +
    +// Replace from to to with what's text (NULL: delete).  Joined paragraphs
    +// keep the first one's alignment; each paragraph what starts ('\n' in
    +// it) takes what's alignment for it.  Returns what's length.
    +int32_t richTextReplace(RichTextT *rt, int32_t from, int32_t to, const RichTextT *what);
    +
    +// Insert plain text at offset at in style (an index of rt's): '\r' is
    +// dropped, a tab becomes RICH_TAB_SPACES spaces, '\n' starts a paragraph
    +// aligned as the one it is in.  Returns the characters inserted.
    +int32_t richTextInsertPlain(RichTextT *rt, int32_t at, const char *text, int32_t len, int32_t style);
    +
    +// rt as plain text in style alone (made the default style).
    +void richTextFromPlain(RichTextT *rt, const char *text, int32_t len, const RichStyleT *style);
    +
    +// Apply change to the styles of from to to.
    +void richTextRestyle(RichTextT *rt, int32_t from, int32_t to, const RichChangeT *change);
    +
    +// style with change applied.
    +RichStyleT richStyleChanged(const RichStyleT *style, const RichChangeT *change);
    +
    +// ============================================================
    +// RTF (richRtf.c)
    +// ============================================================
    +//
    +// A subset: fonts (\fonttbl, \f), sizes (\fs, half points: a pixel is
    +// taken as 3/4 point), \b \i \ul \strike and their ends, \plain, colors
    +// (\colortbl, \cf), paragraphs (\par, \pard, \ql \qc \qr; \line is a
    +// paragraph too), \tab, characters by \'hh (Windows-1252) and \uN, and
    +// the escapes \\ \{ \}.  Other destinations and control words are skipped.
    +// Text in the default family and size is written in font 0 with no size,
    +// and read back as the default.
    +
    +// Whether text is RTF (starts "{\rtf").
    +bool richRtfIs(const char *text, int32_t len);
    +
    +// RTF of rt, NUL-terminated (malloc'd; NULL out of memory); *outLen its
    +// length when outLen is not NULL.  defaultFamily names font 0.
    +char *richRtfWrite(const RichTextT *rt, const char *defaultFamily, int32_t *outLen);
    +
    +// rt as what the RTF text holds.  False, leaving rt alone, when text is
    +// not RTF.
    +bool richRtfRead(RichTextT *rt, const char *text, int32_t len);
    +
    +#endif // RICHTEXT_H
    diff --git a/src/libs/kpunch/richtext/richTextBas.c b/src/libs/kpunch/richtext/richTextBas.c
    new file mode 100644
    index 00000000..8f79ffb2
    --- /dev/null
    +++ b/src/libs/kpunch/richtext/richTextBas.c
    @@ -0,0 +1,94 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// richTextBas.c -- DVX BASIC entry points of the styled text library
    +// (src/include/basic/richtext.bas)
    +//
    +// RTF to plain text and plain text to RTF, for programs that handle RTF
    +// without a RichEdit control (whose TextRTF and SelRTF properties hold
    +// the rest).  Called through DECLARE LIBRARY, so only STRING in and out;
    +// a result stays valid until the next call.
    +
    +#include "richText.h"
    +#include "dvxText.h"
    +#include "stb_ds_wrap.h"
    +
    +#include 
    +#include 
    +
    +static char *sOut = NULL;   // stb_ds: the last result
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +static const char *result(const char *text, int32_t len);
    +const char        *RtfToText(const char *rtf);
    +const char        *TextToRtf(const char *text);
    +
    +
    +// text (len characters) as the result
    +static const char *result(const char *text, int32_t len) {
    +    arrsetlen(sOut, len + 1);
    +    memcpy(sOut, text, (size_t)len);
    +    sOut[len] = '\0';
    +    return sOut;
    +}
    +
    +
    +// The text of RTF, a line per paragraph; text that is not RTF as it is
    +const char *RtfToText(const char *rtf) {
    +    RichTextT rt;
    +    int32_t   len = rtf ? (int32_t)strlen(rtf) : 0;
    +
    +    richTextInit(&rt);
    +
    +    if (!richRtfRead(&rt, rtf, len)) {
    +        richTextFree(&rt);
    +        return result(rtf ? rtf : "", len);
    +    }
    +
    +    const char *out = result(rt.text, richTextLength(&rt));
    +
    +    richTextFree(&rt);
    +    return out;
    +}
    +
    +
    +// Plain text as RTF in the system font, a paragraph per line
    +const char *TextToRtf(const char *text) {
    +    RichTextT  rt;
    +    RichStyleT style;
    +
    +    memset(&style, 0, sizeof(style));
    +    style.color = RICH_COLOR_DEFAULT;
    +    richTextInit(&rt);
    +    richTextFromPlain(&rt, text ? text : "", text ? (int32_t)strlen(text) : 0, &style);
    +
    +    int32_t     len = 0;
    +    char       *rtf = richRtfWrite(&rt, FONT_SYSTEM_FAMILY, &len);
    +    const char *out = result(rtf ? rtf : "", rtf ? len : 0);
    +
    +    free(rtf);
    +    richTextFree(&rt);
    +    return out;
    +}
    diff --git a/src/libs/kpunch/richtext/richtext.dep b/src/libs/kpunch/richtext/richtext.dep
    new file mode 100644
    index 00000000..2afaca0b
    --- /dev/null
    +++ b/src/libs/kpunch/richtext/richtext.dep
    @@ -0,0 +1,2 @@
    +libtasks
    +libdvx
    diff --git a/src/libs/kpunch/richtext/richtext.dhs b/src/libs/kpunch/richtext/richtext.dhs
    new file mode 100644
    index 00000000..0122a36a
    --- /dev/null
    +++ b/src/libs/kpunch/richtext/richtext.dhs
    @@ -0,0 +1,140 @@
    +# The MIT License (MIT)
    +#
    +# Copyright (C) 2026 Scott Duensing
    +#
    +# Permission is hereby granted, free of charge, to any person obtaining a copy
    +# of this software and associated documentation files (the "Software"), to
    +# deal in the Software without restriction, including without limitation the
    +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +# sell copies of the Software, and to permit persons to whom the Software is
    +# furnished to do so, subject to the following conditions:
    +#
    +# The above copyright notice and this permission notice shall be included in
    +# all copies or substantial portions of the Software.
    +#
    +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +# IN THE SOFTWARE.
    +
    +.section Technical Reference (for Developers)|Libraries
    +.topic lib.richtext
    +.title Styled Text Library
    +.toc 0 Styled Text Library
    +.index richtext
    +.index RichTextT
    +.index RichStyleT
    +.index RichRunT
    +.index RichChangeT
    +.index richTextReplace
    +.index richTextCopy
    +.index richTextRestyle
    +.index richRtfRead
    +.index richRtfWrite
    +.index RTF
    +
    +.h1 Styled Text Library
    +
    +Styled text and RTF in and out: the document of the RichEdit widget, and of its undo steps and clipboard. A RichTextT is text in code page 437 with '\n' between paragraphs, covered by runs -- each from an offset to the next run's, in a style (font family, pixel size, bold, italic, underline, strike-through, and color) -- and one alignment per paragraph. It needs no widget: a program can build, edit, and convert styled text with it alone.
    +
    +Header: richtext/richText.h
    +
    +Loaded as: bin/libs/richtext.lib
    +
    +.h2 Types
    +
    +.table
    +  Type          Fields
    +  ----          ------
    +  RichStyleT    family (FONT_FAMILY_MAX; "" = the default family), size (pixels; 0 = the default size), style (FONT_STYLE_*), color (0x00RRGGBB or RICH_COLOR_DEFAULT)
    +  RichRunT      start (the first character's offset), style (an index in RichTextT.styles)
    +  RichTextT     text, runs, styles, aligns: stb_ds arrays; the text has no NUL, the first run starts at 0, styles[0] is the default style, one alignment (FLOW_ALIGN_*) per paragraph
    +  RichChangeT   set (RICH_SET_FAMILY, RICH_SET_SIZE, RICH_SET_COLOR), styleMask (the FONT_STYLE_* bits to set to to.style's), to (the new values)
    +.endtable
    +
    +The default family, size, and color are a RichEdit control's own font and ForeColor, so text in them follows the control. Styles are interned: one look is one index, so neighboring runs never share a style and no run covers nothing (but the one of an empty text). Running out of memory ends the program, as stb_ds does everywhere in DVX.
    +
    +.h2 Constants
    +
    +.table
    +  Constant             Value                Description
    +  --------             -----                -----------
    +  RICH_COLOR_DEFAULT   FLOW_COLOR_DEFAULT   A run in the default color
    +  RICH_TAB_SPACES      4                    The spaces a tab in plain text or RTF becomes (DVX text has no tabs)
    +  RICH_SET_FAMILY      0x0001               RichChangeT.set: change the family
    +  RICH_SET_SIZE        0x0002               RichChangeT.set: change the size
    +  RICH_SET_COLOR       0x0004               RichChangeT.set: change the color
    +.endtable
    +
    +.h2 Styled Text
    +
    +.code
    +void       richTextInit(RichTextT *rt);
    +void       richTextClear(RichTextT *rt);
    +void       richTextFree(RichTextT *rt);
    +int32_t    richTextLength(const RichTextT *rt);
    +int32_t    richTextIntern(RichTextT *rt, const RichStyleT *style);
    +int32_t    richTextRunAt(const RichTextT *rt, int32_t offset);
    +int32_t    richTextStyleAt(const RichTextT *rt, int32_t offset);
    +int32_t    richTextParaCount(const RichTextT *rt);
    +int32_t    richTextParaOf(const RichTextT *rt, int32_t offset);
    +int32_t    richTextParaStart(const RichTextT *rt, int32_t para);
    +int32_t    richTextParaEnd(const RichTextT *rt, int32_t para);
    +int32_t    richTextAlign(const RichTextT *rt, int32_t para);
    +void       richTextSetAlign(RichTextT *rt, int32_t first, int32_t last, int32_t align);
    +void       richTextCopy(const RichTextT *src, int32_t from, int32_t to, RichTextT *dst);
    +int32_t    richTextReplace(RichTextT *rt, int32_t from, int32_t to, const RichTextT *what);
    +int32_t    richTextInsertPlain(RichTextT *rt, int32_t at, const char *text, int32_t len, int32_t style);
    +void       richTextFromPlain(RichTextT *rt, const char *text, int32_t len, const RichStyleT *style);
    +void       richTextRestyle(RichTextT *rt, int32_t from, int32_t to, const RichChangeT *change);
    +RichStyleT richStyleChanged(const RichStyleT *style, const RichChangeT *change);
    +.endcode
    +
    +.table
    +  Function               Description
    +  --------               -----------
    +  richTextInit / Clear   An empty text: one empty, left-aligned paragraph in the default style. Free releases the arrays.
    +  richTextIntern         The index of a style, added when the text has none like it.
    +  richTextRunAt          The run the character at an offset is in (at the end, the last run); richTextStyleAt its style.
    +  richTextPara*          Paragraph count; the paragraph an offset is in (its '\n' belongs to it); where one starts and ends (its '\n' or the text's end); its alignment.
    +  richTextCopy           A range as a text of its own: its characters, runs, the styles they use, and the alignments of the paragraphs it touches.
    +  richTextReplace        Replace a range with a text (NULL deletes). Joined paragraphs keep the first one's alignment; each paragraph the new text starts takes its alignment from it. Returns the length inserted.
    +  richTextInsertPlain    Insert plain text in a style: '\r' dropped, a tab as RICH_TAB_SPACES spaces, '\n' starting a paragraph aligned as the one it is in.
    +  richTextFromPlain      The whole text as plain text in one style, made the default style.
    +  richTextRestyle        Apply a RichChangeT to a range; richStyleChanged applies one to a single style.
    +.endtable
    +
    +Undo works by these: before a change, richTextCopy saves the range it replaces; undoing replaces what the change put there with the saved copy. A copy taken, deleted, and put back gives the text it came from exactly -- characters, runs, styles, and alignments.
    +
    +.h2 RTF
    +
    +.code
    +bool  richRtfIs(const char *text, int32_t len);
    +char *richRtfWrite(const RichTextT *rt, const char *defaultFamily, int32_t *outLen);
    +bool  richRtfRead(RichTextT *rt, const char *text, int32_t len);
    +.endcode
    +
    +richRtfWrite returns a NUL-terminated RTF document (malloc'd; NULL when out of memory): a font table whose font 0 is defaultFamily (the default family is written as font 0 with no size, and read back as the default), a color table whose entry 0 is the default color, then each paragraph as \pard and its alignment, ended by \par, each run as \plain and its whole style. ASCII characters are themselves; the rest are \uN with a '?' for readers without Unicode. richRtfRead replaces rt with what an RTF document holds and returns false, leaving rt alone, when the text is not RTF.
    +
    +The subset read: \fonttbl and \f, \fs (half points; a pixel is taken as 3/4 point, so \fs24 is 16 pixels), \b, \i, \ul, \strike, and their ends, \ulnone, \plain, \colortbl and \cf, \par, \pard, \ql, \qc, \qr (\qj as left; \line as a new paragraph), \tab, \'hh (Windows-1252), \uN with \ucN, the escapes \\ \{ \} \~ \_, and quotes and dashes as their ASCII forms. Characters are stored as code page 437, '?' for those it lacks. Destinations such as \info, \stylesheet, \pict, headers, footers, and every \* group are skipped.
    +
    +.topic lib.richtext.basic
    +.title Styled Text Library: BASIC
    +.toc 1 BASIC
    +.index richtext.bas
    +.index RtfToText$
    +.index TextToRtf$
    +
    +.h1 Styled Text Library: BASIC
    +
    +Source: richtext/richTextBas.c; BASIC declarations: src/include/basic/richtext.bas (DECLARE LIBRARY "richtext").
    +
    +.code
    +const char *RtfToText(const char *rtf);
    +const char *TextToRtf(const char *text);
    +.endcode
    +
    +RtfToText$ is the text of an RTF document, a line per paragraph (text that is not RTF comes back as it is); TextToRtf$ is plain text as RTF in the system font, a paragraph per line. A result stays valid until the next call. The RichEdit control's TextRTF, SelRTF, LoadFile, and SaveFile cover RTF in a control.
    diff --git a/src/libs/kpunch/serial/rs232/rs232.c b/src/libs/kpunch/serial/rs232/rs232.c
    index 0d5f44e7..a041e767 100644
    --- a/src/libs/kpunch/serial/rs232/rs232.c
    +++ b/src/libs/kpunch/serial/rs232/rs232.c
    @@ -218,13 +218,18 @@ DVX_ABI_STAMP(serial);
     #define FCR_TRIGGER_AT_8        FCR_TRIGGER_1
     #define FCR_TRIGGER_AT_14       (FCR_TRIGGER_0 | FCR_TRIGGER_1)
     
    -// Receive FIFO trigger levels accepted by rs232SetFifoThreshold (0 = off)
    +// Receive FIFO trigger levels accepted by rs232SetFifoThreshold (0 = off).
    +// The default interrupts at 8 bytes: the 8 still free in the 16-byte FIFO
    +// last 0.7 ms at 115200 bps.  At 14 the 2 free bytes last 0.17 ms, and the
    +// interrupt latency of a busy DOS machine (a BIOS or DOS call in real
    +// mode) overran it often enough that no full-size secure-link frame
    +// arrived whole (Terminal's transfers stalled on 86Box).
     #define FIFO_THRESHOLD_OFF      0
     #define FIFO_THRESHOLD_1        1
     #define FIFO_THRESHOLD_4        4
     #define FIFO_THRESHOLD_8        8
     #define FIFO_THRESHOLD_14       14
    -#define FIFO_DEFAULT_THRESHOLD  FIFO_THRESHOLD_14
    +#define FIFO_DEFAULT_THRESHOLD  FIFO_THRESHOLD_8
     
     // LCR bits
     #define LCR_DLAB                0x80
    diff --git a/src/libs/kpunch/serial/rs232/rs232.h b/src/libs/kpunch/serial/rs232/rs232.h
    index 963cf481..2a57cf90 100644
    --- a/src/libs/kpunch/serial/rs232/rs232.h
    +++ b/src/libs/kpunch/serial/rs232/rs232.h
    @@ -143,7 +143,8 @@ int32_t rs232Write(int32_t com, const char *data, int32_t len);
     int32_t rs232WriteBuf(int32_t com, const char *data, int32_t len);
     
     // Setters. rs232SetFifoThreshold takes 0 (off), 1, 4, 8 or 14 and needs a
    -// 16550A for any level but 0. rs232SetIrq may be called on an open port.
    +// 16550A for any level but 0 (rs232Open sets 8 on a 16550A, 0 on other
    +// UARTs). rs232SetIrq may be called on an open port.
     // rs232SetBreak holds the line in the break (spacing) state until called
     // again with false; the caller times the break (typically 250-500 ms).
     int32_t rs232Set(int32_t com, int32_t bps, int32_t dataBits, char parity, int32_t stopBits, int32_t handshake);
    diff --git a/src/libs/kpunch/serial/serial.dhs b/src/libs/kpunch/serial/serial.dhs
    index 04559f5f..7c8e6a37 100644
    --- a/src/libs/kpunch/serial/serial.dhs
    +++ b/src/libs/kpunch/serial/serial.dhs
    @@ -258,7 +258,7 @@ rs232GetParity returns a lowercase character on success. Callers comparing again
       rs232GetDsr(com)           DSR line state (0 or 1).
       rs232GetDtr(com)           DTR line state (0 or 1).
       rs232GetRts(com)           RTS line state (0 or 1).
    -  rs232GetLsr(com)           Line status register value as last read; RS232_LSR_OVERRUN, RS232_LSR_PARITY_ERR, RS232_LSR_FRAME_ERR and RS232_LSR_BREAK are its error bits.
    +  rs232GetLsr(com)           Line status register value as last read; RS232_LSR_OVERRUN, RS232_LSR_PARITY_ERR, RS232_LSR_FRAME_ERR, and RS232_LSR_BREAK are its error bits.
       rs232GetMcr(com)           Modem control register value.
       rs232GetMsr(com)           Modem status register value; RS232_MSR_RI is the ring indicator.
     .endtable
    @@ -279,7 +279,7 @@ rs232GetParity returns a lowercase character on success. Callers comparing again
       rs232SetDtr(com, dtr)             Set DTR line state. The state is kept when the port is closed (dropping DTR is how a modem is hung up).
       rs232SetBreak(com, on)            Hold the transmit line in the break (spacing) state (on = true) or release it. The caller times the break, typically 250-500 ms.
       rs232SetRts(com, rts)             Set RTS line state. The state is kept when the port is closed.
    -  rs232SetFifoThreshold(com, thr)   Set FIFO trigger level (1, 4, 8, or 14 bytes) or 0 to turn the FIFO off. Levels other than 0 return RS232_ERR_INVALID_FIFO unless the UART is a 16550A.
    +  rs232SetFifoThreshold(com, thr)   Set FIFO trigger level (1, 4, 8, or 14 bytes) or 0 to turn the FIFO off. Levels other than 0 return RS232_ERR_INVALID_FIFO unless the UART is a 16550A. rs232Open sets 8 on a 16550A (the 8 free bytes allow 0.7 ms of interrupt latency at 115200 bps; 14 allows only 0.17 ms) and 0 on other UARTs.
     .endtable
     
     .h2 Buffer Management
    @@ -499,7 +499,7 @@ Returns PKT_SUCCESS, PKT_ERR_INVALID_PARAM, PKT_ERR_DISCONNECTED, or PKT_ERR_TIM
     
     .h1 Security (DH, XTEA, SHA-256)
     
    -Cryptographic primitives for the serial stack: 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA block cipher in CTR mode, SHA-256, HMAC-SHA256 and HKDF-SHA256, and a SHA-256 based random number generator.
    +Cryptographic primitives for the serial stack: 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA block cipher in CTR mode, SHA-256, HMAC-SHA256, and HKDF-SHA256, and a SHA-256 based random number generator.
     
     XTEA requires no lookup tables and compiles to approximately 20 instructions per round, making it suitable for 486-class hardware where AES S-box tables would thrash the 8KB cache. SHA-256 needs only 32-bit arithmetic and a 256-byte constant table.
     
    @@ -572,7 +572,7 @@ int  secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t i
     bool secConstTimeEqual(const void *a, const void *b, int32_t len);
     .endcode
     
    -SHA-256 follows FIPS 180-4, HMAC RFC 2104 and HKDF RFC 5869 (extract and expand; a NULL or empty salt means 32 zero bytes). secHkdf returns SEC_ERR_PARAM for okmLen outside 1..SEC_HKDF_MAX_OUT. secConstTimeEqual compares without revealing where the inputs differ; use it for every MAC check.
    +SHA-256 follows FIPS 180-4, HMAC RFC 2104, and HKDF RFC 5869 (extract and expand; a NULL or empty salt means 32 zero bytes). secHkdf returns SEC_ERR_PARAM for okmLen outside 1..SEC_HKDF_MAX_OUT. secConstTimeEqual compares without revealing where the inputs differ; use it for every MAC check.
     
     .h2 Diffie-Hellman Key Exchange
     
    @@ -593,7 +593,7 @@ void    secDhDestroy(SecDhT *dh);
       secDhCreate         Allocate a new DH context.
       secDhGenerateKeys   Generate a private exponent and public key from the DRBG.
       secDhGetPublicKey   Copy the local public key into buf. *len receives the key size.
    -  secDhComputeSecret  Compute the shared secret from the remote public key. Keys outside [2, p-2] are rejected with SEC_ERR_PARAM (0, 1 and p-1 would force a trivially known secret).
    +  secDhComputeSecret  Compute the shared secret from the remote public key. Keys outside [2, p-2] are rejected with SEC_ERR_PARAM (0, 1, and p-1 would force a trivially known secret).
       secDhGetSecret      Copy the shared secret (SEC_DH_KEY_SIZE big-endian bytes) into buf. It is input for a key derivation function such as secHkdf, never a key by itself.
       secDhDestroy        Securely zero and free the DH context.
     .endtable
    @@ -624,7 +624,7 @@ secCipherCreate returns NULL if the key pointer is NULL or allocation fails. sec
     
     .h2 BASIC
     
    -src/include/basic/security.bas (DECLARE LIBRARY "serial") declares the BASIC functions (security/secBas.c). Keys, data and results are BINARY STRING (pointer and length in, a result buffer and length out), so every byte value passes; the result buffer is wiped before it is reused. A handle (1, 2, ...; 0 = none) is a key exchange, a cipher or an incremental SHA-256 / HMAC; each records the program that made it and the shell's app-cleanup hook frees (and wipes) what a program leaves open.
    +src/include/basic/security.bas (DECLARE LIBRARY "serial") declares the BASIC functions (security/secBas.c). Keys, data, and results are BINARY STRING (pointer and length in, a result buffer and length out), so every byte value passes; the result buffer is wiped before it is reused. A handle (1, 2, ...; 0 = none) is a key exchange, a cipher, or an incremental SHA-256 / HMAC; each records the program that made it and the shell's app-cleanup hook frees (and wipes) what a program leaves open.
     
     .table
       BASIC                                          C it calls
    @@ -662,7 +662,7 @@ The SEC_ CONSTs of security.bas are compared with security.h and secHash.h by a
     
     .h1 Secure Link
     
    -Top-level API composing all three lower layers (rs232, packet, security) into a single interface. Provides an authenticated key exchange, channel multiplexing (0-127) and per-packet encryption with integrity protection over a reliable serial link.
    +Top-level API composing all three lower layers (rs232, packet, security) into a single interface. Provides an authenticated key exchange, channel multiplexing (0-127), and per-packet encryption with integrity protection over a reliable serial link.
     
     Header: seclink/secLink.h
     
    @@ -685,7 +685,7 @@ Key derivation: HKDF-SHA256 with salt = SHA-256(passphrase) (32 zero bytes witho
     
     Confirmation: each side sends HMAC(confirmation key, SHA-256(lower HELLO + higher HELLO) + role byte). A different passphrase, a passphrase on only one side, or a modified HELLO makes the check fail on both sides with SECLINK_ERR_AUTH. A side that has a passphrase never accepts a peer without one.
     
    -Encrypted frames: encrypt-then-MAC. Each direction numbers its encrypted frames with an implicit 64-bit sequence number that is never sent. The MAC is the first 8 bytes of HMAC-SHA256 over type, channel, big-endian sequence number and ciphertext, and the CTR counter for a frame starts at sequence * 32, so every frame has its own keystream range. Tampered, replayed or injected frames fail the MAC, are dropped and counted (secLinkGetDropped); later frames still verify.
    +Encrypted frames: encrypt-then-MAC. Each direction numbers its encrypted frames with an implicit 64-bit sequence number that is never sent. The MAC is the first 8 bytes of HMAC-SHA256 over type, channel, big-endian sequence number, and ciphertext, and the CTR counter for a frame starts at sequence * 32, so every frame has its own keystream range. Tampered, replayed, or injected frames fail the MAC, are dropped and counted (secLinkGetDropped); later frames still verify.
     
     Without a passphrase the link is encrypted and integrity-protected but not authenticated: an active attacker in the middle could run two separate exchanges. Use the same passphrase on both sides to rule that out.
     
    @@ -744,7 +744,7 @@ SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopB
       com         COM port index (RS232_COM1..RS232_COM4).
       bps         Baud rate.
       dataBits    Data bits per character (5-8).
    -  parity      Parity mode ('N', 'O', 'E', 'M', 'S' or lowercase).
    +  parity      Parity mode ('N', 'O', 'E', 'M', 'S', or lowercase).
       stopBits    Stop bits (1 or 2).
       handshake   Flow control mode (RS232_HANDSHAKE_*).
       callback    Called when a packet is received, after verification and decryption. The data pointer is valid only during the callback.
    @@ -755,7 +755,7 @@ Returns an opaque SecLinkT handle, or NULL on failure (allocation failure, rs232
     
     .h2 secLinkOpenStream
     
    -Open a secure link over a byte stream (see pktOpenStream) instead of a COM port, e.g. a TCP connection to the host proxy's listening port. Everything else -- handshake, passphrase, channels, sending and polling -- is the same as for a COM port link. secLinkClose does not close the stream; the caller closes it afterwards.
    +Open a secure link over a byte stream (see pktOpenStream) instead of a COM port, e.g. a TCP connection to the host proxy's listening port. Everything else -- handshake, passphrase, channels, sending, and polling -- is the same as for a COM port link. secLinkClose does not close the stream; the caller closes it afterwards.
     
     .code
     SecLinkT *secLinkOpenStream(const PktStreamT *stream, SecLinkRecvT callback, void *ctx);
    @@ -807,7 +807,7 @@ Non-blocking handshake for callers that run their own event loop.
     int secLinkHandshakeStep(SecLinkT *link);
     .endcode
     
    -Starts an exchange on an idle link (never handshaken, or dropped by a peer reset), polls once and returns SECLINK_SUCCESS once the link is ready, SECLINK_PENDING (1) while the exchange is still running, or the error that ended it (the same codes as secLinkHandshake, without SECLINK_ERR_TIMEOUT). A failed exchange keeps returning its error; secLinkHandshake starts a new one.
    +Starts an exchange on an idle link (never handshaken, or dropped by a peer reset), polls once, and returns SECLINK_SUCCESS once the link is ready, SECLINK_PENDING (1) while the exchange is still running, or the error that ended it (the same codes as secLinkHandshake, without SECLINK_ERR_TIMEOUT). A failed exchange keeps returning its error; secLinkHandshake starts a new one.
     
     .h2 secLinkSend
     
    @@ -856,7 +856,7 @@ Returns SECLINK_SUCCESS or SECLINK_ERR_PARAM for a bad link or channel.
     
     .h2 secLinkPoll
     
    -Poll for incoming data. Drains the serial port, processes frames and retransmits, completes pending handshake steps, verifies and decrypts data frames and forwards them to the callback.
    +Poll for incoming data. Drains the serial port, processes frames and retransmits, completes pending handshake steps, verifies and decrypts data frames, and forwards them to the callback.
     
     .code
     int secLinkPoll(SecLinkT *link);
    @@ -876,7 +876,7 @@ bool secLinkIsReady(SecLinkT *link);
     
     .h2 secLinkGetDropped
     
    -Number of received frames dropped since open: bad MAC (tampered, replayed or injected), clear frames on a protected channel, encrypted frames without session keys, and malformed frames.
    +Number of received frames dropped since open: bad MAC (tampered, replayed, or injected), clear frames on a protected channel, encrypted frames without session keys, and malformed frames.
     
     .code
     int32_t secLinkGetDropped(SecLinkT *link);
    diff --git a/src/libs/kpunch/sql/dvxsql.dhs b/src/libs/kpunch/sql/dvxsql.dhs
    index ac07858b..3075e66e 100644
    --- a/src/libs/kpunch/sql/dvxsql.dhs
    +++ b/src/libs/kpunch/sql/dvxsql.dhs
    @@ -38,7 +38,7 @@ Header: sql/dvxSql.h
     
     .h3 SQLite Wrapping
     
    -dvxSql wraps the bundled SQLite 3.53.4 amalgamation (sql/thirdparty/sqlite3/sqlite3.c and sqlite3.h; origin, checksum and build options in sql/thirdparty/sqlite3/DVX-VERSION.md). It manages two internal dynamic tables keyed by 1-based handles:
    +dvxSql wraps the bundled SQLite 3.53.4 amalgamation (sql/thirdparty/sqlite3/sqlite3.c and sqlite3.h; origin, checksum, and build options in sql/thirdparty/sqlite3/DVX-VERSION.md). It manages two internal dynamic tables keyed by 1-based handles:
     
     .list
     .item Database table: each slot holds a sqlite3 * plus a per-database error string and affected-row count.
    @@ -53,8 +53,8 @@ SQLite is compiled with the options listed in sql/thirdparty/sqlite3/sqliteOpts.
     
     .list
     .item Double-quoted text is always an identifier (SQLITE_DQS=0): write string literals in single quotes. SELECT "abc" fails with "no such column: abc".
    -.item The JSON functions, WAL journal mode, loadable extensions, shared cache, UTF-16 APIs and deprecated APIs are left out. PRAGMA journal_mode = WAL leaves the database in rollback-journal (DELETE) mode.
    -.item Temporary tables, sorts and statement journals are kept in memory (PRAGMA temp_store = FILE switches a connection to temporary files).
    +.item The JSON functions, WAL journal mode, loadable extensions, shared cache, UTF-16 APIs, and deprecated APIs are left out. PRAGMA journal_mode = WAL leaves the database in rollback-journal (DELETE) mode.
    +.item Temporary tables, sorts, and statement journals are kept in memory (PRAGMA temp_store = FILE switches a connection to temporary files).
     .item New databases use 1 KB pages; each connection caches up to 512 KB. Existing databases keep their own page size.
     .item LIKE never matches BLOB values.
     .endlist
    @@ -66,11 +66,11 @@ The DOS port is SQLite's SQLITE_OS_OTHER build with a small DVX file layer, the
     .item Journals keep 8.3 names: DATA.DB uses DATA.NAL (the last three letters of "-journal"), and an extensionless DATA uses DATA.NAL too. Two databases that differ only in extension (DATA.DB and DATA.DAT) share the journal name, so keep them in different directories.
     .item Paths are made absolute and canonical (relative names join the current directory, backslashes become forward slashes, "." and ".." are resolved), so every spelling of one file is the same database.
     .item Locking is in-process: DOS is single user, but two connections inside DVX (two programs, or a Data control and BASIC code) can share a file. Any number may read; one may write. A write while another connection is writing, or a commit while another connection has a query open, fails with "database is locked"; retry after the other side finishes (dvxSqlFreeResult releases a query's read lock). Nothing is locked at the DOS level, so do not share a database with another program outside DVX.
    -.item Temporary files, when used, are SQLxxxxx.TMP in the directory named by TMPDIR, TEMP or TMP (else the current directory) and are deleted when closed.
    +.item Temporary files, when used, are SQLxxxxx.TMP in the directory named by TMPDIR, TEMP, or TMP (else the current directory) and are deleted when closed.
     .item Randomness comes from the serial library's generator (secRngBytes) when that library is loaded, else from platformEntropyStamp mixed with the time of day.
     .endlist
     
    -Databases written by the SQLite 3.5.6 that earlier DVX releases bundled open, read and write unchanged. Files created now use the current file format, which SQLite 3.5.6 cannot read.
    +Databases written by the SQLite 3.5.6 that earlier DVX releases bundled open, read, and write unchanged. Files created now use the current file format, which SQLite 3.5.6 cannot read.
     
     .h3 Handle Model
     
    @@ -172,7 +172,7 @@ Return the last error message for a database handle. The returned string is stor
       db           Database handle.
     .endtable
     
    -Returns the error string, or "Invalid database handle" if the handle is invalid. Messages are SQLite's own (for example "no such table: x" or "database is locked"); when a file could not be opened, read or written, the operating system's reason follows in parentheses. Handle 0 (what a failed dvxSqlOpen returns) reports why the most recent open failed; after a successful open it is invalid again.
    +Returns the error string, or "Invalid database handle" if the handle is invalid. Messages are SQLite's own (for example "no such table: x" or "database is locked"); when a file could not be opened, read, or written, the operating system's reason follows in parentheses. Handle 0 (what a failed dvxSqlOpen returns) reports why the most recent open failed; after a successful open it is invalid again.
     
     .h3 dvxSqlAffectedRows
     
    diff --git a/src/libs/kpunch/texthelp/textHelp.c b/src/libs/kpunch/texthelp/textHelp.c
    index 508b1002..5426ce49 100644
    --- a/src/libs/kpunch/texthelp/textHelp.c
    +++ b/src/libs/kpunch/texthelp/textHelp.c
    @@ -1439,7 +1439,9 @@ void wgtUpdateCursorBlink(void) {
             sCursorBlinkTime = now;
             sCursorBlinkOn   = !sCursorBlinkOn;
     
    -        if (sFocusedWidget) {
    +        // A widget with a quick repaint (Terminal, RichEdit) blinks its own
    +        // caret and repaints only the caret's rows
    +        if (sFocusedWidget && !wclsHas(sFocusedWidget, WGT_METHOD_QUICK_REPAINT)) {
                 wgtInvalidatePaint(sFocusedWidget);
             }
         }
    @@ -2141,9 +2143,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditSingleT *
             return;
         }
     
    -    AppContextT *ctx = wgtGetContext(w);
    -
    -    textEditScrollToCursor(te->cursorPos, textEditVisibleChars(fieldWidth > 0 ? fieldWidth : w->w, &ctx->font), &te->scrollOff);
    +    textEditScrollToCursor(te->cursorPos, textEditVisibleChars(fieldWidth > 0 ? fieldWidth : w->w, wgtFont(w)), &te->scrollOff);
         wgtInvalidatePaint(w);
     }
     
    diff --git a/src/libs/kpunch/texthelp/textHelp.h b/src/libs/kpunch/texthelp/textHelp.h
    index 84bf55c5..25731e68 100644
    --- a/src/libs/kpunch/texthelp/textHelp.h
    +++ b/src/libs/kpunch/texthelp/textHelp.h
    @@ -401,12 +401,13 @@ bool    textEditReplaceSelectionMulti(TextEditLineCacheT *lc, char *buf, int32_t
     //
     // widgetTextEditOnKey runs one keystroke through the engine (clipboard,
     // undo, navigation, editing, Shift selection), fires w->onChange when the
    -// text changed, keeps the cursor visible and repaints.  fieldWidth is the
    -// field's full width (0 = w->w).  widgetTextEditMouseClick places the
    -// cursor (double click = word, or everything when wordSelect is false;
    -// triple click = everything) and starts a drag selection when dragSelect
    -// is set; widgetTextEditDragUpdateLine extends it, auto-scrolling past
    -// either edge.
    +// text changed, keeps the cursor visible (in columns of w's font, wgtFont)
    +// and repaints.  fieldWidth is the field's full width (0 = w->w).
    +// widgetTextEditMouseClick places the cursor (double click = word, or
    +// everything when wordSelect is false; triple click = everything) and
    +// starts a drag selection when dragSelect is set;
    +// widgetTextEditDragUpdateLine extends it, auto-scrolling past either
    +// edge.
     typedef struct TextEditSingleT {
         char    *buf;
         char    *undoBuf;
    diff --git a/src/libs/kpunch/texthelp/texthelp.dhs b/src/libs/kpunch/texthelp/texthelp.dhs
    index 993fad50..4a1b22b6 100644
    --- a/src/libs/kpunch/texthelp/texthelp.dhs
    +++ b/src/libs/kpunch/texthelp/texthelp.dhs
    @@ -45,10 +45,10 @@ Widget DXEs that implement text editing delegate to textHelp for the four high-c
     .item Cursor blink. The library tracks a 250 ms blink timer in a static global and the focused widget reads sCursorBlinkOn (exposed via libdvx) when repainting.
     .item Selection clearing. When a widget gains focus it calls clearOtherSelections(self) so only one widget ever has an active text selection.
     .item Word boundaries. isWordChar, wordStart/wordEnd, and wordBoundaryLeft/wordBoundaryRight implement the logic for double-click word selection and Ctrl+Left/Right navigation in a uniform way.
    -.item Single-line editing engine. A single-line widget (TextInput, Spinner, ComboBox) embeds a TextEditSingleT (buffer, cursor, scroll offset, selection, undo slot) and hands it to widgetTextEditOnKey, widgetTextEditMouseClick and widgetTextEditDragUpdateLine. widgetTextEditPaintLine draws a line from an explicit buffer so a widget can paint substitute text (password bullets).
    +.item Single-line editing engine. A single-line widget (TextInput, Spinner, ComboBox) embeds a TextEditSingleT (buffer, cursor, scroll offset, selection, undo slot) and hands it to widgetTextEditOnKey, widgetTextEditMouseClick, and widgetTextEditDragUpdateLine. widgetTextEditPaintLine draws a line from an explicit buffer so a widget can paint substitute text (password bullets).
     .endlist
     
    -The multi-line engine (TextArea) works the same way with a TextEditMultiT: widgetTextEditMultiOnKey(w, key, mod, te, visRows, visCols, opts) runs a key on it. Its clipboard and undo commands are functions of their own, which the keys and a TextArea's Copy / Cut / Paste / SelectAll / Undo / SelText run alike: textEditMultiCopy (false when nothing is selected), textEditMultiCut, textEditMultiPaste, textEditMultiReplaceSel(te, text, textLen) (replaces the selection or inserts at the cursor, as much as fits, undo saved first), textEditMultiSelectAll and textEditMultiUndo (a second one is redo). The editing ones return true when the text changed; the caller then fires Change, scrolls to the cursor and repaints.
    +The multi-line engine (TextArea) works the same way with a TextEditMultiT: widgetTextEditMultiOnKey(w, key, mod, te, visRows, visCols, opts) runs a key on it. Its clipboard and undo commands are functions of their own, which the keys and a TextArea's Copy / Cut / Paste / SelectAll / Undo / SelText run alike: textEditMultiCopy (false when nothing is selected), textEditMultiCut, textEditMultiPaste, textEditMultiReplaceSel(te, text, textLen) (replaces the selection or inserts at the cursor, as much as fits, undo saved first), textEditMultiSelectAll, and textEditMultiUndo (a second one is redo). The editing ones return true when the text changed; the caller then fires Change, scrolls to the cursor, and repaints.
     
     .h2 Constants
     
    @@ -319,7 +319,7 @@ typedef struct TextEditSingleT {
       uint32_t textEditDefaultSyntaxRgb(uint8_t idx)                            Default syntax color (0x00RRGGBB) for a TEXT_SYNTAX_* index; 0 for TEXT_SYNTAX_DEFAULT, which uses the caller's foreground.
     .endtable
     
    -The key handler, the drag update and the widgets' paint code all use textEditVisibleChars, so they agree on the visible width.
    +The key handler, the drag update, and the widgets' paint code all use textEditVisibleChars, so they agree on the visible width.
     
     .topic lib.texthelp.dragupdate
     .title widgetTextEditDragUpdateLine
    @@ -338,7 +338,7 @@ void widgetTextEditDragUpdateLine(TextEditSingleT *te, int32_t vx, int32_t leftE
     .table
       Parameter     Type                Description
       ---------     ----                -----------
    -  te            TextEditSingleT *   [in/out] Editor state: cursorPos, scrollOff and selEnd are updated.
    +  te            TextEditSingleT *   [in/out] Editor state: cursorPos, scrollOff, and selEnd are updated.
       vx            int32_t             Mouse x position in viewport coordinates.
       leftEdge      int32_t             Pixel x of the left edge of the text area.
       maxChars      int32_t             Maximum visible characters in the field.
    @@ -371,7 +371,7 @@ void widgetTextEditMouseClick(WidgetT *w, TextEditSingleT *te, int32_t vx, int32
       Parameter     Type                Description
       ---------     ----                -----------
       w             WidgetT *           The widget receiving the click.
    -  te            TextEditSingleT *   [in/out] Editor state: cursorPos, selStart and selEnd are set.
    +  te            TextEditSingleT *   [in/out] Editor state: cursorPos, selStart, and selEnd are set.
       vx            int32_t             Mouse x in viewport coordinates.
       vy            int32_t             Mouse y in viewport coordinates.
       textLeftX     int32_t             Pixel x of the left edge of the text area.
    @@ -391,7 +391,7 @@ void widgetTextEditMouseClick(WidgetT *w, TextEditSingleT *te, int32_t vx, int32
     
     .h2 widgetTextEditOnKey
     
    -The core single-line text editing engine. Processes a single keystroke and updates the editor's buffer, cursor, scroll offset, selection, and undo state. Shared by TextInput, Spinner and ComboBox.
    +The core single-line text editing engine. Processes a single keystroke and updates the editor's buffer, cursor, scroll offset, selection, and undo state. Shared by TextInput, Spinner, and ComboBox.
     
     .code
     void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditSingleT *te, int32_t fieldWidth);
    @@ -401,7 +401,7 @@ void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditSingleT *
       Parameter     Type                Description
       ---------     ----                -----------
       w             WidgetT *           The widget receiving the key event.
    -  key           int32_t             Key code (ASCII for printable, control code or scan|0x100 for extended).
    +  key           int32_t             Key code (ASCII for printable, control code, or scan|0x100 for extended).
       mod           int32_t             Modifier flags (KEY_MOD_SHIFT, etc.).
       te            TextEditSingleT *   [in/out] Editor state. Ctrl+Z does nothing while undoLen is TEXT_EDIT_UNDO_NONE.
       fieldWidth    int32_t             Field width in pixels including the text padding (subtracted internally). 0 = use widget width.
    diff --git a/src/libs/kpunch/xfer/xfer.dhs b/src/libs/kpunch/xfer/xfer.dhs
    index 73b34859..1e3159f2 100644
    --- a/src/libs/kpunch/xfer/xfer.dhs
    +++ b/src/libs/kpunch/xfer/xfer.dhs
    @@ -32,7 +32,7 @@
     
     .h1 File Transfer Library
     
    -XMODEM, XMODEM-1K, YMODEM and ZMODEM senders and receivers that work over any byte stream: a serial port, the secure link, and later a network connection. The engines are non-blocking state machines: start a transfer with read and write callbacks, then call xferPoll with the current time in milliseconds as often as you can (every frame). A poll hands queued output to the transport, consumes the input that has arrived and runs the timeouts. Nothing blocks and nothing touches the screen, so a program stays responsive and draws its own progress.
    +XMODEM, XMODEM-1K, YMODEM, and ZMODEM senders and receivers that work over any byte stream: a serial port, the secure link, and later a network connection. The engines are non-blocking state machines: start a transfer with read and write callbacks, then call xferPoll with the current time in milliseconds as often as you can (every frame). A poll hands queued output to the transport, consumes the input that has arrived, and runs the timeouts. Nothing blocks and nothing touches the screen, so a program stays responsive and draws its own progress.
     
     Loaded as: bin/libs/kpunch/xfer/xfer.lib (needs the serial library only for the BASIC functions)
     
    @@ -87,7 +87,7 @@ if (xferPoll(x, nowMs()) != XferRunningE) {
       XferT          Opaque transfer. One XferT runs one transfer at a time and can be started again when it has ended.
       XferIoT        The transport: ctx, read (never blocks; returns the bytes available, 0 when none), write (returns the bytes accepted, may be fewer), pending (optional: bytes accepted but not yet sent; a ZMODEM sender keeps it small so it reacts quickly to an error report on a slow line).
       XferOptionsT   exists (what happens to an incoming file that already exists), rxWindow (ZMODEM receiving: bytes the sender may stream before it waits; 0 = no limit), escapeCtl (ZMODEM: escape every control character), resume (ZMODEM sending: ask the receiver to continue partial files).
    -  XferProgressT  Status, protocol, direction, file index and count, file size and bytes done, batch size and bytes done, resume offset, retries, current file name, and the outcome message. While the status is XferAskingE, askLocalSize, askRemoteSize and askCanResume describe the question.
    +  XferProgressT  Status, protocol, direction, file index and count, file size and bytes done, batch size and bytes done, resume offset, retries, current file name, and the outcome message. While the status is XferAskingE, askLocalSize, askRemoteSize, and askCanResume describe the question.
       XferDetectT    State of the ZMODEM auto-start detector (zero it, or call xferDetectReset).
     .endtable
     
    @@ -113,10 +113,10 @@ if (xferPoll(x, nowMs()) != XferRunningE) {
       bool xferStartReceive(x, proto, io, opt, target, now)     XMODEM: target is the file to create. YMODEM and ZMODEM: target is the folder for the files.
       XferStatusE xferPoll(x, now)                              Move the transfer along. now is a millisecond clock (wrapping is fine).
       void xferCancel(x)                                        Send the cancel sequence to the other side and end with XferCancelledE.
    -  void xferAnswer(x, action, now)                           Answer XferAskingE: XferExistResumeE, RenameE, OverwriteE or SkipE.
    +  void xferAnswer(x, action, now)                           Answer XferAskingE: XferExistResumeE, RenameE, OverwriteE, or SkipE.
       const XferProgressT *xferProgress(x)                      Progress and outcome.
       bool xferDetectFeed(d, byte)                              Feed received terminal bytes; true when a ZMODEM sender's start frame (ZRQINIT) has arrived, so a terminal can start a ZMODEM receive by itself.
    -  void xferSafeName(name, longNames, out)                   Reduce a sender's file name to a safe one: no path, no drive, no DOS device name (CON, PRN, COM1 and so on get a leading _). With longNames (the receiving disk takes long names, platformLongNames) a long name keeping its case, blanks and dots, where a character a long name cannot hold becomes _ and leading blanks and trailing dots and blanks go (RECEIVED when nothing is left); otherwise an 8.3 name of portable characters in upper case. out holds XFER_NAME_MAX bytes (PLATFORM_LONG_NAME_MAX + 1).
    +  void xferSafeName(name, longNames, out)                   Reduce a sender's file name to a safe one: no path, no drive, no DOS device name (CON, PRN, COM1, and so on get a leading _). With longNames (the receiving disk takes long names, platformLongNames) a long name keeping its case, blanks, and dots, where a character a long name cannot hold becomes _ and leading blanks and trailing dots and blanks go (RECEIVED when nothing is left); otherwise an 8.3 name of portable characters in upper case. out holds XFER_NAME_MAX bytes (PLATFORM_LONG_NAME_MAX + 1).
       uint16_t xferCrc16(data, len)                             CRC-16/XMODEM (polynomial 0x1021).
       uint32_t xferCrc32(data, len)                             CRC-32 (IEEE 802.3), as ZMODEM uses it.
       int32_t xferBasNew(&x)                                    For the BASIC functions of other libraries: a new xfer.bas handle owning a new idle transfer (in x) that belongs to the running program; 0 when no handle is free. The library starts the transfer over its own transport; the program follows it with the Xfer functions (the connection library's ConnSendFiles and ConnReceiveFiles work this way).
    @@ -146,13 +146,13 @@ A batch of files, always with CRC and 1K blocks (a tail of at most 128 bytes goe
     
     .h2 ZMODEM
     
    -Headers are sent in hex (to start and answer) or binary with CRC-16 or CRC-32; data travels in subpackets with a CRC over the data and the frame end. ZDLE escaping protects ZDLE, DLE, XON and XOFF (with and without the high bit) and a CR after '@'; with escapeCtl every control character is escaped (the sender announces it with ZSINIT; the receiver asks for it in ZRINIT). Five CANs in a row cancel.
    +Headers are sent in hex (to start and answer) or binary with CRC-16 or CRC-32; data travels in subpackets with a CRC over the data and the frame end. ZDLE escaping protects ZDLE, DLE, XON, and XOFF (with and without the high bit), and a CR after '@'; with escapeCtl every control character is escaped (the sender announces it with ZSINIT; the receiver asks for it in ZRINIT). Five CANs in a row cancel.
     
     The sender streams: it keeps at most 8 KB unacknowledged, asks for a progress acknowledgement every 4 KB, honours the receiver's buffer size (a receiver without flow control should set rxWindow to what its hardware buffer holds, so a slow frame never loses data), and builds a subpacket only when the transport has sent nearly everything it was given (XferIoT.pending). On an error the receiver sends ZRPOS with the last good position; the sender goes back there and halves its subpacket size (down to 64 bytes), then doubles it again after eight clean subpackets. A sender that gets no acknowledgement for the window goes back to the last acknowledged position. File names (long names as they are) travel in the ZFILE subpacket with the size and the files and bytes left in the batch, which the receiver shows as batch progress. Commands from the other side (ZCOMMAND) are never run.
     
     Files are at most 2 GB (sizes and positions are 32-bit): a send refuses a larger file, a size offered past 2 GB reads as unknown, and a receive that passes 2 GB stops with "A file larger than 2 GB cannot be transferred". A file already on the disk is never mistaken for a missing one, whatever its size.
     
    -Interoperability: the engines are tested against lrz and lsz (lrzsz 0.12.20) in both directions for every protocol, including escaping, resume and long file names.
    +Interoperability: the engines are tested against lrz and lsz (lrzsz 0.12.20) in both directions for every protocol, including escaping, resume, and long file names.
     
     .topic lib.xfer.basic
     .title File Transfers from BASIC
    @@ -173,7 +173,7 @@ Transfers over a connection (conn.bas: ConnSendFiles, ConnReceiveFiles) are the
       --------                             -----------
       XferSendFile(com, proto, path)       Send one file over COM port com (1-4). Returns a handle, or 0.
       XferReceive(com, proto, target)      Receive: XMODEM into the file target; YMODEM and ZMODEM into the folder target. Returns a handle, or 0.
    -  XferPoll(handle)                     Move the transfer along. Returns XFER_RUNNING, XFER_ASKING, XFER_DONE, XFER_FAILED or XFER_CANCELLED.
    +  XferPoll(handle)                     Move the transfer along. Returns XFER_RUNNING, XFER_ASKING, XFER_DONE, XFER_FAILED, or XFER_CANCELLED.
       XferStatus(handle)                   The same, without moving it along (XFER_IDLE for a bad handle).
       XferSending(handle), XferProtocol(handle)  True for a send; the protocol.
       XferBytes(handle), XferSize(handle)  Bytes of the current file done, and its size (-1 when unknown).
    @@ -183,7 +183,7 @@ Transfers over a connection (conn.bas: ConnSendFiles, ConnReceiveFiles) are the
       XferResumedAt(handle), XferErrors(handle)  Where the current file continued from; retries so far.
       XferAskSize(handle), XferCanResume(handle)  XFER_ASKING: the size of the file already there; True when XFER_EXIST_RESUME can continue it.
       XferAskRemoteSize(handle)            XFER_ASKING: the size the sender offers (XferProgressT.askRemoteSize; -1 when unknown).
    -  XferAnswer handle, action            Answer XFER_ASKING with XFER_EXIST_RENAME, _RESUME, _OVERWRITE or _SKIP.
    +  XferAnswer handle, action            Answer XFER_ASKING with XFER_EXIST_RENAME, _RESUME, _OVERWRITE, or _SKIP.
       XferMessage$(handle)                 How the transfer ended, or why it failed.
       XferCancel handle                    Stop a running transfer (the other side is told).
       XferClose handle                     Free the handle.
    diff --git a/src/sdk/README.TXT b/src/sdk/README.TXT
    index 4c8e5c16..68bc3cc0 100644
    --- a/src/sdk/README.TXT
    +++ b/src/sdk/README.TXT
    @@ -48,6 +48,8 @@ Directory Structure
                        dvxres     resource tool (adds icons and text to modules)
                        dvxhlpc    help compiler (.dhs -> .hlp)
                        bascomp    DVX BASIC compiler (.dbp -> .app)
    +                   mkfont     font maker (TrueType, Type 1, and bitmap
    +                              fonts -> .DFN, for a program to carry)
                        dxeGen.sh  runs DJGPP's dxe3gen safely under make -j
       include/
         core/        DVX core headers (types, drawing, windows, widgets)
    @@ -57,6 +59,7 @@ Directory Structure
         serial/      Serial port, packet, security and secure link APIs
         texthelp/    Shared text editing helpers for widgets
         listhelp/    Shared list and dropdown helpers for widgets
    +    richtext/    Styled text and RTF in and out (RichEdit widget)
         fileops/     File operations: drives, attributes, listing, copy/move/delete jobs
         xfer/        File transfer protocols: XMODEM, YMODEM, ZMODEM
         conn/        Serial, modem and secure-link connections behind one interface
    diff --git a/src/sdk/templates/game/tplName.dhs b/src/sdk/templates/game/tplName.dhs
    index a2d2a379..1a78606f 100644
    --- a/src/sdk/templates/game/tplName.dhs
    +++ b/src/sdk/templates/game/tplName.dhs
    @@ -18,7 +18,7 @@ Collect as many coins as you can before the time runs out. Describe here what yo
     .item Move with the arrow keys or a joystick.
     .item Hold Ctrl, or the first joystick button, to move faster.
     .item Touch a coin to collect it. A new one appears somewhere else.
    -.item Each round lasts 30 seconds. Your score, the time left and your best score are shown in the title bar.
    +.item Each round lasts 30 seconds. Your score, the time left, and your best score are shown in the title bar.
     .item Press F2 to play again.
     .endlist
     
    diff --git a/src/sdk/templates/library/tplName.dhs b/src/sdk/templates/library/tplName.dhs
    index 74c2db3a..1c14ff57 100644
    --- a/src/sdk/templates/library/tplName.dhs
    +++ b/src/sdk/templates/library/tplName.dhs
    @@ -30,4 +30,4 @@ Header: tplName.h
     
     .h2 BASIC
     
    -Add tplName.bas to a project, then call tplNameAdd, tplNameGreet$ and tplNameXor$ like any other function. tplNameXor$ is declared with BINARY STRING, so its argument and result may hold any bytes, CHR$(0) included.
    +Add tplName.bas to a project, then call tplNameAdd, tplNameGreet$, and tplNameXor$ like any other function. tplNameXor$ is declared with BINARY STRING, so its argument and result may hold any bytes, CHR$(0) included.
    diff --git a/src/sdk/templates/widget/tplName.bhs b/src/sdk/templates/widget/tplName.bhs
    index ba3bdaa6..d670d141 100644
    --- a/src/sdk/templates/widget/tplName.bhs
    +++ b/src/sdk/templates/widget/tplName.bhs
    @@ -15,7 +15,7 @@ Shows its Caption centred in a box with a border.
     .table
       Property   Type   Description
       --------   ----   -----------
    -  Style      Enum   Flat, Raised or Sunken border.
    +  Style      Enum   Flat, Raised, or Sunken border.
     .endtable
     
     .h2 Methods
    diff --git a/src/sdk/templates/widget/tplName.dhs b/src/sdk/templates/widget/tplName.dhs
    index 301827bc..1b4e5250 100644
    --- a/src/sdk/templates/widget/tplName.dhs
    +++ b/src/sdk/templates/widget/tplName.dhs
    @@ -10,7 +10,7 @@
     
     .h2 TplName
     
    -Shows a line of text centred in a box with a flat, raised or sunken border.
    +Shows a line of text centred in a box with a flat, raised, or sunken border.
     
     Header: tplName.h
     
    diff --git a/src/sdkHeaders.mk b/src/sdkHeaders.mk
    index 7ccd782b..b5e9395f 100644
    --- a/src/sdkHeaders.mk
    +++ b/src/sdkHeaders.mk
    @@ -30,12 +30,13 @@
     # SDK_ROOT is the repository root as seen from the including Makefile,
     # with a trailing slash (empty at the top level).
     
    -SDK_CORE_H     = $(addprefix $(SDK_ROOT)src/libs/kpunch/libdvx/,dvxApp.h dvxTypes.h dvxWgt.h dvxWgtP.h dvxWm.h dvxDraw.h dvxVideo.h dvxComp.h dvxPrefs.h dvxDlg.h dvxRes.h dvxFont.h dvxCur.h dvxPal.h dvxMem.h dvxDrv.h platform/dvxPlat.h)
    +SDK_CORE_H     = $(addprefix $(SDK_ROOT)src/libs/kpunch/libdvx/,dvxApp.h dvxTypes.h dvxWgt.h dvxWgtP.h dvxWm.h dvxDraw.h dvxVideo.h dvxComp.h dvxPrefs.h dvxDlg.h dvxRes.h dvxFont.h dvxText.h dvxFlow.h dvxCur.h dvxPal.h dvxMem.h dvxDrv.h platform/dvxPlat.h)
     SDK_SHELL_H    = $(addprefix $(SDK_ROOT)src/libs/kpunch/dvxshell/,shellApp.h shellAssoc.h shellInf.h shellProg.h shellRun.h pmModel.h) $(SDK_ROOT)src/libs/kpunch/taskmgr/shellTaskMgr.h
     SDK_TASKS_H    = $(SDK_ROOT)src/libs/kpunch/libtasks/taskSwch.h
     SDK_SQL_H      = $(SDK_ROOT)src/libs/kpunch/sql/dvxSql.h
     SDK_TEXTHELP_H = $(SDK_ROOT)src/libs/kpunch/texthelp/textHelp.h
     SDK_LISTHELP_H = $(SDK_ROOT)src/libs/kpunch/listhelp/listHelp.h
    +SDK_RICHTEXT_H = $(SDK_ROOT)src/libs/kpunch/richtext/richText.h
     SDK_FILEOPS_H  = $(SDK_ROOT)src/libs/kpunch/fileops/fileOps.h
     SDK_HLPFILE_H  = $(addprefix $(SDK_ROOT)src/libs/kpunch/hlpfile/,hlpFile.h hlpFormat.h)
     SDK_XFER_H     = $(SDK_ROOT)src/libs/kpunch/xfer/xfer.h
    diff --git a/src/test/abi/Makefile b/src/test/abi/Makefile
    index aaca509f..510465ec 100644
    --- a/src/test/abi/Makefile
    +++ b/src/test/abi/Makefile
    @@ -40,7 +40,7 @@ include ../../sdkHeaders.mk
     
     OBJDIR    = $(HOST_OBJROOT)/test/abi
     TABLE     = abiSizes.txt
    -HEADERS   = $(SDK_CORE_H) $(SDK_SHELL_H) $(SDK_TASKS_H) $(SDK_SQL_H) $(SDK_TEXTHELP_H) $(SDK_LISTHELP_H) $(SDK_FILEOPS_H) $(SDK_HLPFILE_H) $(SDK_XFER_H) $(SDK_CONN_H) $(SDK_PRINT_H) $(SDK_GAME_H) $(SDK_JOYSTICK_H) $(SDK_AUDIO_H) $(SDK_MUSIC_H) $(addprefix $(SDK_SERIAL_DIR)/,$(SDK_SERIAL_H)) $(SDK_WIDGET_H) $(SDK_NET_H)
    +HEADERS   = $(SDK_CORE_H) $(SDK_SHELL_H) $(SDK_TASKS_H) $(SDK_SQL_H) $(SDK_TEXTHELP_H) $(SDK_LISTHELP_H) $(SDK_RICHTEXT_H) $(SDK_FILEOPS_H) $(SDK_HLPFILE_H) $(SDK_XFER_H) $(SDK_CONN_H) $(SDK_PRINT_H) $(SDK_GAME_H) $(SDK_JOYSTICK_H) $(SDK_AUDIO_H) $(SDK_MUSIC_H) $(addprefix $(SDK_SERIAL_DIR)/,$(SDK_SERIAL_H)) $(SDK_WIDGET_H) $(SDK_NET_H)
     ABI_FLAGS = $(WARNFLAGS) -march=i486 -I$(DVX_ROOT)/src/libs/kpunch/libdvx/thirdparty
     
     .PHONY: all check update clean
    diff --git a/src/test/abi/abiSizes.txt b/src/test/abi/abiSizes.txt
    index 9b594d4b..95803de8 100644
    --- a/src/test/abi/abiSizes.txt
    +++ b/src/test/abi/abiSizes.txt
    @@ -2,12 +2,12 @@
     # (i386 DJGPP), recorded at the DVX_ABI_VERSION below.  Generated by
     # `make -C src/test/abi update`; read abiCheck.py and DVX_ABI_VERSION in
     # src/libs/kpunch/libdvx/dvxTypes.h before changing it.
    -version 8
    +version 9
     AccelEntryT           12
     AccelTableT           12
     AnsiTermApiT          36
     AnsiTermExApiT        16
    -AppContextT           3436
    +AppContextT           3480
     AppDescriptorT        80
     AudioDriverT          28
     AudioDrvConfigT       16
    @@ -21,7 +21,7 @@ AudioStatsT           24
     AudioSynthDriverT     36
     AudioSynthDrvInfoT    72
     BevelStyleT           16
    -BitmapFontT           20
    +BitmapFontT           64
     BlitOpsT              12
     BoxApiT               20
     ButtonApiT            12
    @@ -39,6 +39,7 @@ DropdownApiT          44
     DvxDriverClassT       16
     DvxDriverHeaderT      16
     DvxDriverModT         12
    +DvxFontChoiceT        40
     DvxResDirEntryT       48
     DvxResFooterT         16
     DvxResHandleT         268
    @@ -48,6 +49,8 @@ FileFilterT           4
     FileOpsCallbacksT     12
     FileOpsEntryT         20
     FileOpsProgressT      32
    +FontGlyphT            6
    +FontStampT            8
     GameFramePacerT       24
     GamePalCacheT         37632
     HelpViewApiT          32
    @@ -72,8 +75,8 @@ LabelApiT             16
     ListBoxApiT           68
     ListIconCellT         36
     ListIconGridT         80
    -ListItemsT            20
    -ListPopupT            36
    +ListItemsT            28
    +ListPopupT            44
     ListSelT              16
     ListViewApiT          84
     ListViewColT          12
    @@ -104,6 +107,12 @@ ProgressBarApiT       32
     RadioApiT             36
     RawKeyEventT          8
     RectT                 16
    +RichChangeT           52
    +RichEditApiT          132
    +RichLabelApiT         40
    +RichRunT              8
    +RichStyleT            44
    +RichTextT             16
     ScrollPaneApiT        40
     ScrollbarT            32
     SecHmacT              216
    @@ -139,7 +148,7 @@ WgtPropDescT          20
     WgtSigInfoT           7
     WgtValueT             12
     WidgetClassT          136
    -WidgetT               240
    +WidgetT               244
     WindowStackT          60
     WindowT               308
     WrapBoxApiT           12
    diff --git a/src/test/basdev/testBasDev.c b/src/test/basdev/testBasDev.c
    index f2b47f08..c33aadf3 100644
    --- a/src/test/basdev/testBasDev.c
    +++ b/src/test/basdev/testBasDev.c
    @@ -126,6 +126,7 @@ const char *CompTokens(const char *source);
     const char *CompWithEvents(const char *source);
     const char *DbgExec(int32_t run, const char *line);
     int32_t     ProjAddFile(int32_t prj, const char *fileName);
    +int32_t     ProjAddFont(int32_t prj, const char *fileName);
     void        ProjClose(int32_t prj);
     const char *ProjCtlHeader(const char *text, const char *key);
     const char *ProjCtlKeys(void);
    @@ -136,11 +137,14 @@ int32_t     ProjFileCount(int32_t prj);
     int32_t     ProjFileFind(int32_t prj, const char *fileName);
     int32_t     ProjFileKind(int32_t prj, int32_t idx);
     const char *ProjFilePath(int32_t prj, int32_t idx);
    +const char *ProjFont(int32_t prj, int32_t idx);
    +int32_t     ProjFontCount(int32_t prj);
     const char *ProjGet(int32_t prj, const char *key);
     int32_t     ProjNew(const char *name, const char *folder);
     int32_t     ProjOpen(const char *dbpPath);
     const char *ProjPath(int32_t prj);
     int32_t     ProjRemoveFile(int32_t prj, int32_t idx);
    +int32_t     ProjRemoveFont(int32_t prj, int32_t idx);
     int32_t     ProjRenameFile(int32_t prj, int32_t idx, const char *newName);
     int32_t     ProjSave(int32_t prj);
     int32_t     ProjSet(int32_t prj, const char *key, const char *value);
    @@ -191,6 +195,7 @@ static void        caseDesignControls(void);
     static void        caseIncludeConsts(void);
     static void        caseProjectApi(void);
     static void        caseProjectControls(void);
    +static void        caseProjectFonts(void);
     static void        caseSourceApi(void);
     static bool        copyKid(const char *kidDir);
     static void        designProject(const BasFormRtT *rt);
    @@ -623,6 +628,10 @@ static void caseDesignControls(void) {
     static void caseIncludeConsts(void) {
         static const BdtConstT consts[] = {
             { "resource.bas", "RES_NAME_MAX",              DVX_RES_NAME_MAX          },
    +        { "resource.bas", "RES_TYPE_ICON",             DVX_RES_ICON              },
    +        { "resource.bas", "RES_TYPE_TEXT",             DVX_RES_TEXT              },
    +        { "resource.bas", "RES_TYPE_BINARY",           DVX_RES_BINARY            },
    +        { "resource.bas", "RES_TYPE_FONT",             DVX_RES_FONT              },
             { "system.bas",   "MOUSE_WHEEL_STEP_MIN",      MOUSE_WHEEL_STEP_MIN      },
             { "system.bas",   "MOUSE_WHEEL_STEP_MAX",      MOUSE_WHEEL_STEP_MAX      },
             { "system.bas",   "MOUSE_WHEEL_STEP_DEFAULT",  MOUSE_WHEEL_STEP_DEFAULT  },
    @@ -991,6 +1000,52 @@ static void caseProjectControls(void) {
     }
     
     
    +// The fonts a program carries: listed once each, saved in [Fonts] and
    +// read back in order, removed (the file staying)
    +static void caseProjectFonts(void) {
    +    tCase("compiler.bas projects: the fonts a program carries");
    +    tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    +
    +    char dir[DVX_MAX_PATH];
    +    char dbp[DVX_MAX_PATH];
    +
    +    snprintf(dir, sizeof(dir), "%s/projfonts", tTempDir());
    +    snprintf(dbp, sizeof(dbp), "%s/fonts.dbp", dir);
    +    platformMkdirRecursive(dir);
    +    remove(dbp);
    +
    +    int32_t prj = ProjNew("fonts", dir);
    +
    +    TEST_TRUE(prj > 0);
    +    TEST_EQ(ProjAddFont(prj, "title.dfn"), 0);
    +    TEST_EQ(ProjAddFont(prj, "body.dfn"), 1);
    +    TEST_EQ(ProjAddFont(prj, "TITLE.DFN"), NO_INDEX);
    +    TEST_TRUE(CompError()[0] != '\0');
    +    TEST_EQ(ProjAddFont(prj, ""), NO_INDEX);
    +    TEST_EQ(ProjFontCount(prj), 2);
    +    TEST_STR_EQ(ProjFont(prj, 1), "body.dfn");
    +    TEST_STR_EQ(ProjFont(prj, 2), "");
    +    TEST_EQ(ProjSave(prj), BAS_TRUE);
    +    ProjClose(prj);
    +
    +    int32_t len  = 0;
    +    char   *text = platformReadFile(dbp, &len);
    +
    +    TEST_TRUE(text && strstr(text, "[Fonts]") && strstr(text, "File1 = body.dfn"));
    +    free(text);
    +    prj = ProjOpen(dbp);
    +    TEST_EQ(ProjFontCount(prj), 2);
    +    TEST_STR_EQ(ProjFont(prj, 0), "title.dfn");
    +    TEST_EQ(ProjRemoveFont(prj, 0), BAS_TRUE);
    +    TEST_EQ(ProjRemoveFont(prj, 5), 0);
    +    TEST_EQ(ProjFontCount(prj), 1);
    +    TEST_STR_EQ(ProjFont(prj, 0), "body.dfn");
    +    ProjClose(prj);
    +    TEST_EQ(ProjFontCount(prj), 0);
    +    tShutdown();
    +}
    +
    +
     // Source text as the compiler reads it: tokens, code positions, renaming
     // in code, WithEvents variables, class Events, help topics, keyword
     // classes.
    @@ -1371,6 +1426,7 @@ int main(void) {
         caseSourceApi();
         caseProjectApi();
         caseProjectControls();
    +    caseProjectFonts();
         caseCDebugger();
         caseBasicIde();
     
    diff --git a/src/test/dialogs/Makefile b/src/test/dialogs/Makefile
    index d5e10615..975257f8 100644
    --- a/src/test/dialogs/Makefile
    +++ b/src/test/dialogs/Makefile
    @@ -24,8 +24,6 @@
     # SAN=1 (default) for ASan/UBSan; SAN=0 plain.  UPDATE_GOLDENS=1 make run
     # regenerates the golden crops under golden/.
     
    -DVXTEST_GOLDEN_DIR := $(abspath golden)
    -
     include ../dvxtest/dvxtest.mk
     
     .DEFAULT_GOAL := all
    diff --git a/src/test/dos/golden/telnet/86box/audio.json b/src/test/dos/golden/telnet/86box/audio.json
    new file mode 100644
    index 00000000..f4052b80
    --- /dev/null
    +++ b/src/test/dos/golden/telnet/86box/audio.json
    @@ -0,0 +1,43 @@
    +{
    + "windowMs": 20,
    + "freqs": [
    +  440,
    +  523,
    +  659
    + ],
    + "sounds": [
    +  {
    +   "startMs": 0,
    +   "lengthMs": 460,
    +   "level": 0.11651,
    +   "loudest": 440,
    +   "spectrum": [
    +    0.15046,
    +    0.02538,
    +    0.00974
    +   ]
    +  },
    +  {
    +   "startMs": 1000,
    +   "lengthMs": 460,
    +   "level": 0.1156,
    +   "loudest": 523,
    +   "spectrum": [
    +    0.0264,
    +    0.15001,
    +    0.01374
    +   ]
    +  },
    +  {
    +   "startMs": 2000,
    +   "lengthMs": 460,
    +   "level": 0.11428,
    +   "loudest": 659,
    +   "spectrum": [
    +    0.01015,
    +    0.0141,
    +    0.14943
    +   ]
    +  }
    + ]
    +}
    diff --git a/src/test/dos/golden/telnet/dosbox/audio.json b/src/test/dos/golden/telnet/dosbox/audio.json
    new file mode 100644
    index 00000000..19c6b84c
    --- /dev/null
    +++ b/src/test/dos/golden/telnet/dosbox/audio.json
    @@ -0,0 +1,43 @@
    +{
    + "windowMs": 20,
    + "freqs": [
    +  440,
    +  523,
    +  659
    + ],
    + "sounds": [
    +  {
    +   "startMs": 0,
    +   "lengthMs": 400,
    +   "level": 0.17993,
    +   "loudest": 440,
    +   "spectrum": [
    +    0.23477,
    +    0.04063,
    +    0.01545
    +   ]
    +  },
    +  {
    +   "startMs": 940,
    +   "lengthMs": 460,
    +   "level": 0.17247,
    +   "loudest": 523,
    +   "spectrum": [
    +    0.04207,
    +    0.21662,
    +    0.0223
    +   ]
    +  },
    +  {
    +   "startMs": 1940,
    +   "lengthMs": 460,
    +   "level": 0.17014,
    +   "loudest": 659,
    +   "spectrum": [
    +    0.01569,
    +    0.02082,
    +    0.22202
    +   ]
    +  }
    + ]
    +}
    diff --git a/src/test/dos/netServers.py b/src/test/dos/netServers.py
    index daba7b78..3e652fef 100644
    --- a/src/test/dos/netServers.py
    +++ b/src/test/dos/netServers.py
    @@ -30,7 +30,9 @@
     #   127.0.0.1:2323  Telnet test server: sends IAC DO TTYPE, DO NAWS,
     #                   WILL ECHO, WILL SGA, SB TTYPE SEND; once it has read
     #                   IAC SB TTYPE IS "ANSI" IAC SE and IAC SB NAWS it sends
    -#                   "\r\nDVX TELNET TEST OK\r\n", waits and closes.
    +#                   "\r\nDVX TELNET TEST OK\r\n", an ANSI music tune
    +#                   (A4, C5, E5 a beat apart, played in the foreground)
    +#                   and "TUNE DONE\r\n", waits and closes.
     #
     # The guest reaches the host as 10.0.2.2 on both emulators' SLiRP
     # networks.  The ports are fixed because the DOS side names them; a port
    @@ -48,6 +50,11 @@ RECV_MAX = 4096
     LISTEN_BACKLOG = 5
     TELNET_DEADLINE_SECONDS = 30
     TELNET_LINGER_SECONDS = 2
    +# ANSI music (ESC[MF ... Ctrl+N): quarter notes at 120 beats a minute,
    +# each followed by a quarter rest so every note is a sound of its own
    +# (telnet.scr's # AUDIO line: 440, 523, and 659 Hz; octave 3 of PLAY
    +# starts at middle C)
    +TELNET_TUNE = b"\x1b[MFT120L4O3AP4O4CP4EP4\x0e"
     
     IAC = 255
     DONT = 254
    @@ -101,7 +108,7 @@ def telnet(conn):
     
                 if want_type in received and want_size in received:
                     log.write("telnet: got ANSI and window size\n")
    -                conn.sendall(b"\r\nDVX TELNET TEST OK\r\n")
    +                conn.sendall(b"\r\nDVX TELNET TEST OK\r\n" + TELNET_TUNE + b"TUNE DONE\r\n")
                     time.sleep(TELNET_LINGER_SECONDS)
                     return
     
    diff --git a/src/test/dos/runDos.sh b/src/test/dos/runDos.sh
    index 62e4b244..50d087e0 100755
    --- a/src/test/dos/runDos.sh
    +++ b/src/test/dos/runDos.sh
    @@ -58,6 +58,11 @@
     # staged folder as C: and uses its own mouse driver.  Both emulators run
     # under Xvfb with HOME and the XDG folders inside toolchains/ (emuEnv.sh).
     # Needs ./installToolchain.sh, xvfb-run, mtools, flock and python3.
    +#
    +# DOSBOX_CYCLES=N runs DOSBox at a fixed N cycles a millisecond instead of
    +# flat out, in real and protected mode (cpu_cycles and
    +# cpu_cycles_protected): slowness that only 86Box shows, found without its
    +# long runs.
     
     # The whole script is one { } block so bash reads all of it before running
     # any of it: editing this file while a long run is in progress cannot
    @@ -256,7 +261,8 @@ DOSBOX_MAPPER_LINE=""
     #           whenever Setup logs that it waits for a disk.
     # C:\DVX\TEST holds only the script module, the script and the fixtures.
     # Setup's log (C:\SETUP.LOG) is copied out and must say "Setup: finished"
    -# and every "# SETUPLOG: text" line's text.  Setup also gets the test-only
    +# and every "# SETUPLOG: text" line's text.  "# SETUPARGS: args" adds args
    +# to Setup's command line (e.g. /FONTS=BASE).  Setup also gets the test-only
     # /SHOTS=C:\SUSHOTS: it shows each of its pages and questions (upgrade,
     # disk prompts) and saves them there (SU-*.BMP); those pictures are copied
     # out with the script's own and compared with the goldens the same way
    @@ -271,6 +277,7 @@ SETUP_LOG_NAME="SETUP.LOG"
     SETUP_SHOTS_DIR="SUSHOTS"
     SETUP_DONE_LINE="Setup: finished"
     SETUP_ARGS="/AUTO=${INSTALL_DEST} /LOG=C:\\${SETUP_LOG_NAME} /SHOTS=C:\\${SETUP_SHOTS_DIR}"
    +SCRIPT_SETUP_ARGS="${SETUP_ARGS}"
     CD_DRIVE="D"
     FLOPPY_DRIVE="A"
     DISKSWAP="${HERE}/diskSwap.py"
    @@ -674,9 +681,9 @@ installStage() {
                 DOSBOX_INSTALL_TEXT="${DOSBOX_INSTALL_TEXT} \"${images[n]}\""
             done
     
    -        DOSBOX_INSTALL_TEXT="${DOSBOX_INSTALL_TEXT} -t floppy"$'\n'"${FLOPPY_DRIVE}:\\SETUP.EXE ${SETUP_ARGS}"$'\n'
    +        DOSBOX_INSTALL_TEXT="${DOSBOX_INSTALL_TEXT} -t floppy"$'\n'"${FLOPPY_DRIVE}:\\SETUP.EXE ${SCRIPT_SETUP_ARGS}"$'\n'
         else
    -        DOSBOX_INSTALL_TEXT="imgmount ${CD_DRIVE} \"${INSTALL_ISO}\" -t iso"$'\n'"${CD_DRIVE}:\\SETUP.EXE ${SETUP_ARGS}"$'\n'
    +        DOSBOX_INSTALL_TEXT="imgmount ${CD_DRIVE} \"${INSTALL_ISO}\" -t iso"$'\n'"${CD_DRIVE}:\\SETUP.EXE ${SCRIPT_SETUP_ARGS}"$'\n'
         fi
     
         if [ "${INSTALL_UPGRADE}" -eq 1 ]; then
    @@ -887,7 +894,8 @@ machine = ${SCRIPT_DOSBOX_MACHINE}
     memsize = ${DOSBOX_MEM_MB}
     
     [cpu]
    -cpu_cycles = max
    +cpu_cycles = ${DOSBOX_CYCLES:-max}
    +${DOSBOX_CYCLES:+cpu_cycles_protected = ${DOSBOX_CYCLES}}
     ${DOSBOX_CPU_LINE}
     
     [mixer]
    @@ -1581,6 +1589,7 @@ for script in "${SCRIPTS[@]}"; do
         INSTALL_TEXT=""
         INSTALL_CONFIG=""
         DOSBOX_INSTALL_TEXT=""
    +    SCRIPT_SETUP_ARGS="${SETUP_ARGS}$(sed -n 's/^#[[:space:]]*SETUPARGS:[[:space:]]*\(.*[^[:space:]]\)[[:space:]]*$/ \1/p' "${script}" | head -n 1)"
     
         if [ "${installFlag}" = "upgrade" ]; then
             INSTALL_UPGRADE=1
    @@ -1598,7 +1607,7 @@ for script in "${SCRIPTS[@]}"; do
     
         if [ "${EMU}" = "86box" ] && [ -n "${INSTALL_MODE}" ]; then
             INSTALL_CONFIG="DEVICE=C:\\$(basename "${XMS_DRIVER}")"$'\n'"DEVICE=C:\\$(basename "${CD_DRIVER}") /D:${CD_DEVICE}"$'\n'
    -        INSTALL_TEXT="C:\\$(basename "${CD_EXTENSION}") /D:${CD_DEVICE}"$'\n'"${CD_DRIVE}:\\SETUP.EXE ${SETUP_ARGS}"$'\n'
    +        INSTALL_TEXT="C:\\$(basename "${CD_EXTENSION}") /D:${CD_DEVICE}"$'\n'"${CD_DRIVE}:\\SETUP.EXE ${SCRIPT_SETUP_ARGS}"$'\n'
         fi
     
         SETUPLOG_EXPECTS=()
    diff --git a/src/test/dos/scripts/fonts.scr b/src/test/dos/scripts/fonts.scr
    new file mode 100644
    index 00000000..760f9bad
    --- /dev/null
    +++ b/src/test/dos/scripts/fonts.scr
    @@ -0,0 +1,43 @@
    +# fonts.scr -- fonts on the real DOS build at the test mode's 16 bits per
    +# pixel, where text is smoothed: the BASIC Demo's Fonts window (a
    +# RichLabel, a label in a font of its own, and a RichEdit), DVX Demo's
    +# Fonts tab (labels in fonts of their own and a RichLabel), and its Rich
    +# Text tab (a RichEdit typed into, its second line made bold).
    +TIMEOUT 300
    +WAITWIN "Program Manager"
    +MOVE 639 479
    +
    +LAUNCH apps/kpunch/basdemo/basdemo.app
    +WAITWIN "DVX BASIC Feature Tour"
    +WAIT 20
    +KEY Alt+D F
    +WAITWIN "Fonts"
    +WAIT 20
    +MOVE 639 479
    +SHOT basic
    +KEY Alt+F4
    +WAITNOWIN "Fonts"
    +KEY Alt+F4
    +WAITNOWIN "DVX BASIC Feature Tour"
    +
    +LAUNCH apps/kpunch/dvxdemo/dvxdemo.app
    +WAITWIN "Advanced Widgets"
    +WAIT 20
    +# Maximized (a double click on its title), then the Fonts tab (Alt+O)
    +DBLCLICK "Advanced Widgets" 150 10
    +WAIT 20
    +KEY Alt+O
    +WAIT 20
    +MOVE 639 479
    +SHOT demo
    +# The Rich Text tab (Alt+X): a click in the RichEdit, a word typed at
    +# its top, its second line selected and made bold
    +KEY Alt+X
    +WAIT 20
    +CLICK "Advanced Widgets" 300 120
    +KEY Ctrl+Home
    +TYPE "Smooth "
    +KEY Down Home Shift+End Ctrl+B
    +WAIT 20
    +SHOT edit
    +QUIT
    diff --git a/src/test/dos/scripts/fonts8.scr b/src/test/dos/scripts/fonts8.scr
    new file mode 100644
    index 00000000..21f34600
    --- /dev/null
    +++ b/src/test/dos/scripts/fonts8.scr
    @@ -0,0 +1,41 @@
    +# fonts8.scr -- the same as fonts.scr at 8 bits per pixel, where text is
    +# not smoothed: the 1-bit glyphs of every font (the smoothed sizes too).
    +# VIDEO: 640 480 8
    +TIMEOUT 300
    +WAITWIN "Program Manager"
    +MOVE 639 479
    +
    +LAUNCH apps/kpunch/basdemo/basdemo.app
    +WAITWIN "DVX BASIC Feature Tour"
    +WAIT 20
    +KEY Alt+D F
    +WAITWIN "Fonts"
    +WAIT 20
    +MOVE 639 479
    +SHOT basic
    +KEY Alt+F4
    +WAITNOWIN "Fonts"
    +KEY Alt+F4
    +WAITNOWIN "DVX BASIC Feature Tour"
    +
    +LAUNCH apps/kpunch/dvxdemo/dvxdemo.app
    +WAITWIN "Advanced Widgets"
    +WAIT 20
    +# Maximized (a double click on its title), then the Fonts tab (Alt+O)
    +DBLCLICK "Advanced Widgets" 150 10
    +WAIT 20
    +KEY Alt+O
    +WAIT 20
    +MOVE 639 479
    +SHOT demo
    +# The Rich Text tab (Alt+X): a click in the RichEdit, a word typed at
    +# its top, its second line selected and made bold
    +KEY Alt+X
    +WAIT 20
    +CLICK "Advanced Widgets" 300 120
    +KEY Ctrl+Home
    +TYPE "Smooth "
    +KEY Down Home Shift+End Ctrl+B
    +WAIT 20
    +SHOT edit
    +QUIT
    diff --git a/src/test/dos/scripts/instbase.scr b/src/test/dos/scripts/instbase.scr
    new file mode 100644
    index 00000000..a5bdb6cf
    --- /dev/null
    +++ b/src/test/dos/scripts/instbase.scr
    @@ -0,0 +1,44 @@
    +# instbase.scr -- SETUP.EXE installs DVX with only the base set of fonts.
    +#
    +# As install.scr, from the CD image, with /FONTS=BASE: Setup unpacks DVX
    +# and the base set (DejaVu Sans, Sans Mono, and Serif) and stops before
    +# the other fonts, and must log the choice.  The installed DVX then shows
    +# what it has: the BASIC Demo's Fonts window counts 4 font families (the
    +# system font and the three DejaVu ones), and its texts in other families
    +# are drawn in those; DVX Demo's Fonts tab draws Pacifico all the same,
    +# in the size the demo carries as a font resource.
    +# INSTALL: cd
    +# SETUPARGS: /FONTS=BASE
    +# SETUPLOG: installing in C:\DVX (new), fonts BASE
    +# LOG: DVX Shell ready.
    +TIMEOUT 300
    +WAITWIN "Welcome to DVX"
    +MOVE 639 479
    +KEY Alt+C
    +WAITNOWIN "Welcome to DVX"
    +ASSERTWIN "Program Manager"
    +
    +LAUNCH apps/kpunch/basdemo/basdemo.app
    +WAITWIN "DVX BASIC Feature Tour"
    +WAIT 20
    +KEY Alt+D F
    +WAITWIN "Fonts"
    +WAIT 20
    +MOVE 639 479
    +SHOT fonts
    +KEY Alt+F4
    +WAITNOWIN "Fonts"
    +KEY Alt+F4
    +WAITNOWIN "DVX BASIC Feature Tour"
    +
    +LAUNCH apps/kpunch/dvxdemo/dvxdemo.app
    +WAITWIN "Advanced Widgets"
    +WAIT 20
    +# Maximized (a double click on its title), then the Fonts tab (Alt+O)
    +DBLCLICK "Advanced Widgets" 150 10
    +WAIT 20
    +KEY Alt+O
    +WAIT 20
    +MOVE 639 479
    +SHOT demo
    +QUIT
    diff --git a/src/test/dos/scripts/telnet.scr b/src/test/dos/scripts/telnet.scr
    index 8208caec..9260ef6f 100644
    --- a/src/test/dos/scripts/telnet.scr
    +++ b/src/test/dos/scripts/telnet.scr
    @@ -10,6 +10,13 @@
     # terminal type "ANSI" and a window size.  Terminal is started with
     # telnet://10.0.2.2:2323, which adds that host to its phone book and
     # connects at once.
    +#
    +# The banner ends with an ANSI music tune (ESC[MF, A4, C5 and E5 a beat
    +# apart, Ctrl+N) and "TUNE DONE": Terminal plays it through the music
    +# library on the Sound Blaster 16 the # AUDIO line gives the emulator,
    +# holding the text after it until it ends, and the recording must hold
    +# the three notes in order (the first is the sync tone).
    +# AUDIO: 440 523 659
     TIMEOUT 180
     WAITWIN "Program Manager"
     MOVE 639 479
    diff --git a/src/test/dos/scripts/ucide.scr b/src/test/dos/scripts/ucide.scr
    index b3440cdc..6ba77ca6 100644
    --- a/src/test/dos/scripts/ucide.scr
    +++ b/src/test/dos/scripts/ucide.scr
    @@ -34,7 +34,9 @@ WAITMS 5000
     MOVE 799 599
     WAIT 20
     SHOT ucide-tbx
    -CLICK "Toolbox" 28 371
    +# The user control's tool: row 9, column 3 (the tools are in name order,
    +# so a new widget moves it).
    +CLICK "Toolbox" 92 301
     WAIT 5
     CLICK "UCIDE [Design]" C C
     WAITLOG "UCIDE INIT 2 DESIGN"
    diff --git a/src/test/dos/scripts/video.scr b/src/test/dos/scripts/video.scr
    index bc9a2f93..6fe98209 100644
    --- a/src/test/dos/scripts/video.scr
    +++ b/src/test/dos/scripts/video.scr
    @@ -4,7 +4,8 @@
     # list), Apply Mode, and keep it at the "Keep this mode?" prompt: DVX must
     # come back up at 800x600 with its windows, and the prompt must go away.
     # Positions are relative to the Control Panel window (the Apply Mode
    -# button from its bottom edge), so they survive tabs being added.
    +# button from its bottom edge, above the text smoothing checkbox), so they
    +# survive tabs being added.
     TIMEOUT 120
     WAITWIN "Program Manager"
     MOVE 639 479
    @@ -17,7 +18,7 @@ WAIT 10
     KEY 8
     WAIT 10
     SHOT v-pick
    -CLICK "Control Panel" C B-55
    +CLICK "Control Panel" C B-85
     WAITWIN "Keep this mode?"
     WAIT 20
     KEY Enter
    diff --git a/src/test/dvxtest/dvxtest.c b/src/test/dvxtest/dvxtest.c
    index 5c8f3bd7..a0784ace 100644
    --- a/src/test/dvxtest/dvxtest.c
    +++ b/src/test/dvxtest/dvxtest.c
    @@ -25,6 +25,8 @@
     #include "dvxtest.h"
     #include "dvxWgtP.h"
     #include "dvxVideo.h"
    +#include "checkbox/checkbox.h"
    +#include "listBox/listBox.h"
     #include "slider/slider.h"
     #include "stb_ds_wrap.h"
     
    @@ -36,6 +38,7 @@
     #include 
     #include 
     #include 
    +#include 
     #include 
     
     // ============================================================
    @@ -76,6 +79,12 @@
     
     #define SPY_LOG_BUF_SIZE       4096
     
    +// dvxFontDialog's lists, left to right (tFontDialogPick)
    +#define FONT_DLG_FAMILY        0
    +#define FONT_DLG_STYLE         1
    +#define FONT_DLG_SIZE          2
    +#define FONT_DLG_LISTS         3
    +
     #ifndef DVXTEST_GOLDEN_DIR
     #define DVXTEST_GOLDEN_DIR     "golden"
     #endif
    @@ -807,6 +816,65 @@ void tFKey(int32_t n) {
     }
     
     
    +bool tFontDialogPick(WindowT *modal, const char *family, int32_t size, uint32_t style) {
    +    // dvxFontDialog's root holds a row of three columns, each a label
    +    // over a list (family, style, size), then a row of the Underline and
    +    // Strikeout check boxes.
    +    WidgetT *lists     = (modal && modal->widgetRoot) ? modal->widgetRoot->firstChild : NULL;
    +    WidgetT *underline = (lists && lists->nextSibling) ? lists->nextSibling->firstChild : NULL;
    +    WidgetT *strike    = underline ? underline->nextSibling : NULL;
    +    WidgetT *column    = lists ? lists->firstChild : NULL;
    +    WidgetT *list[FONT_DLG_LISTS];
    +
    +    for (int32_t i = 0; i < FONT_DLG_LISTS; i++) {
    +        list[i] = (column && column->firstChild) ? column->firstChild->nextSibling : NULL;
    +        column  = column ? column->nextSibling : NULL;
    +
    +        if (!list[i] || !list[i]->onChange) {
    +            return false;
    +        }
    +    }
    +
    +    if (!strike || !underline->onChange || !strike->onChange) {
    +        return false;
    +    }
    +
    +    int32_t found = -1;
    +
    +    for (int32_t i = 0; i < wgtListBoxGetItemCount(list[FONT_DLG_FAMILY]); i++) {
    +        if (strcasecmp(wgtListBoxGetItem(list[FONT_DLG_FAMILY], i), family) == 0) {
    +            found = i;
    +        }
    +    }
    +
    +    if (found < 0) {
    +        return false;
    +    }
    +
    +    wgtListBoxSetSelected(list[FONT_DLG_FAMILY], found);
    +    list[FONT_DLG_FAMILY]->onChange(list[FONT_DLG_FAMILY]);
    +    wgtListBoxSetSelected(list[FONT_DLG_STYLE], (int32_t)(style & FONT_STYLE_FACE));
    +    list[FONT_DLG_STYLE]->onChange(list[FONT_DLG_STYLE]);
    +
    +    // The size the family has nearest the one asked for
    +    int32_t best = 0;
    +
    +    for (int32_t i = 0; i < wgtListBoxGetItemCount(list[FONT_DLG_SIZE]); i++) {
    +        if (abs(atoi(wgtListBoxGetItem(list[FONT_DLG_SIZE], i)) - size) < abs(atoi(wgtListBoxGetItem(list[FONT_DLG_SIZE], best)) - size)) {
    +            best = i;
    +        }
    +    }
    +
    +    wgtListBoxSetSelected(list[FONT_DLG_SIZE], best);
    +    list[FONT_DLG_SIZE]->onChange(list[FONT_DLG_SIZE]);
    +    wgtCheckboxSetChecked(underline, (style & FONT_STYLE_UNDERLINE) != 0);
    +    underline->onChange(underline);
    +    wgtCheckboxSetChecked(strike, (style & FONT_STYLE_STRIKE) != 0);
    +    strike->onChange(strike);
    +    return tDialogClickButton(modal, "OK");
    +}
    +
    +
     bool tGoldenRect(const char *name, int32_t x, int32_t y, int32_t w, int32_t h) {
         const DisplayT *d  = &sCtx->display;
         const uint8_t  *fb = hostFramebuffer(d);
    diff --git a/src/test/dvxtest/dvxtest.h b/src/test/dvxtest/dvxtest.h
    index 9140d75e..821fe5d7 100644
    --- a/src/test/dvxtest/dvxtest.h
    +++ b/src/test/dvxtest/dvxtest.h
    @@ -226,6 +226,12 @@ bool tDialogClickButton(WindowT *modal, const char *caption);
     // like the color dialog.
     bool tColorDialogPick(WindowT *modal, uint32_t rgb);
     
    +// In a dvxFontDialog modal: choose family (by name, any case), the size
    +// it has nearest size and style (FONT_STYLE_* flags), and choose OK.
    +// False when the dialog does not look like the font dialog or does not
    +// offer family.
    +bool tFontDialogPick(WindowT *modal, const char *family, int32_t size, uint32_t style);
    +
     // ============================================================
     // Golden images
     // ============================================================
    diff --git a/src/test/dvxtest/dvxtest.mk b/src/test/dvxtest/dvxtest.mk
    index 12089d1e..12012555 100644
    --- a/src/test/dvxtest/dvxtest.mk
    +++ b/src/test/dvxtest/dvxtest.mk
    @@ -39,9 +39,13 @@
     # stand-in for the Clock program, which is BASIC), and for a suite that
     # runs a BASIC program: DVXTEST_BASSTUB_SO (the BASIC stub as a host .so;
     # the program's resources are appended to a copy) and the phony
    -# dvxtest-bascomp (the host compiler, DVXTEST_BASCOMP).  Each archive rule always runs its
    -# library's sub-make, and make re-reads the archive's time stamp
    -# afterwards, so dependents relink exactly when the archive changed.
    +# dvxtest-bascomp (the host compiler, DVXTEST_BASCOMP); for a suite that
    +# draws with the fonts, the phony dvxtest-fonts (DVXTEST_FONTS, bin/fonts
    +# as src/tools makes it: link it into the suite's working folder as
    +# DVXTEST_FONTS_DIR, where the font engine looks).  Each archive rule
    +# always runs its library's sub-make, and make re-reads the archive's
    +# time stamp afterwards, so dependents relink exactly when the archive
    +# changed.
     #
     # SAN / COV select the instrumentation and the object tree (common.mk):
     # SAN=1 (default) ASan/UBSan under obj/host, SAN=0 under obj/hostplain,
    @@ -58,6 +62,7 @@ DVXTEST_LIBDVX    := $(DVXTEST_ROOT)/src/libs/kpunch/libdvx
     DVXTEST_LIBTASKS  := $(DVXTEST_ROOT)/src/libs/kpunch/libtasks
     DVXTEST_TEXTHELP  := $(DVXTEST_ROOT)/src/libs/kpunch/texthelp
     DVXTEST_LISTHELP  := $(DVXTEST_ROOT)/src/libs/kpunch/listhelp
    +DVXTEST_RICHTEXT  := $(DVXTEST_ROOT)/src/libs/kpunch/richtext
     DVXTEST_HLPFILE   := $(DVXTEST_ROOT)/src/libs/kpunch/hlpfile
     DVXTEST_SQLDIR    := $(DVXTEST_ROOT)/src/libs/kpunch/sql
     DVXTEST_BASICDIR  := $(DVXTEST_ROOT)/src/apps/kpunch/dvxbasic
    @@ -74,10 +79,14 @@ DVXTEST_ARCHIVES  := $(DVXTEST_LIBDVX_A) $(DVXTEST_LIB)
     DVXTEST_BASSTUB_SO:= $(DVXTEST_BINDIR)/basstub.so
     DVXTEST_BASCOMP   := $(DVXTEST_ROOT)/bin/host/bascomp
     DVXTEST_BASCOMPREL:= ../../../../bin/host/bascomp
    +DVXTEST_FONTS     := $(DVXTEST_ROOT)/bin/fonts
    +DVXTEST_FONTSREL  := ../../obj/tools/fonts.stamp
    +# dvxText.h's DVX_FONTS_DIR
    +DVXTEST_FONTS_DIR := FONTS
     DVXTEST_CLOCK_SRC := $(DVXTEST_ROOT)/src/test/shell/fixtures/fixClock.c
     DVXTEST_CLOCK_SO  := $(DVXTEST_OBJDIR)/apps/clock.so
     
    -DVXTEST_INCLUDES  := -I$(DVXTEST_DIR) -I$(DVXTEST_LIBDVX) -I$(DVXTEST_LIBDVX)/platform -I$(DVXTEST_LIBDVX)/thirdparty -I$(DVXTEST_LIBTASKS) -I$(DVXTEST_WIDGETS) -I$(DVXTEST_TEXTHELP) -I$(DVXTEST_LISTHELP)
    +DVXTEST_INCLUDES  := -I$(DVXTEST_DIR) -I$(DVXTEST_LIBDVX) -I$(DVXTEST_LIBDVX)/platform -I$(DVXTEST_LIBDVX)/thirdparty -I$(DVXTEST_LIBTASKS) -I$(DVXTEST_WIDGETS) -I$(DVXTEST_TEXTHELP) -I$(DVXTEST_LISTHELP) -I$(DVXTEST_RICHTEXT)
     # The help compiler's every-directive fixture (src/test/tools), compiled
     # once for the suites that read help files (hlpfile, widgets, formrt).
     DVXTEST_HLPC      := $(DVXTEST_ROOT)/bin/host/dvxhlpc
    @@ -90,10 +99,10 @@ DVXTEST_LINK      := $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_LIBDVX_A)
     
     # Sources folded into libdvxtest.a: the runner plus the helper libraries
     # widget .so files resolve from the executable at load time.
    -DVXTEST_LIB_SRCS  := $(DVXTEST_DIR)/dvxtest.c $(DVXTEST_TEXTHELP)/textHelp.c $(wildcard $(DVXTEST_LISTHELP)/*.c) $(wildcard $(DVXTEST_HLPFILE)/*.c)
    +DVXTEST_LIB_SRCS  := $(DVXTEST_DIR)/dvxtest.c $(DVXTEST_TEXTHELP)/textHelp.c $(wildcard $(DVXTEST_LISTHELP)/*.c) $(wildcard $(DVXTEST_RICHTEXT)/*.c) $(wildcard $(DVXTEST_HLPFILE)/*.c)
     DVXTEST_LIB_OBJS  := $(patsubst %.c,$(DVXTEST_BINDIR)/lib/%.o,$(notdir $(DVXTEST_LIB_SRCS)))
     
    -.PHONY: dvxtest-libs dvxtest-widgets dvxtest-apps dvxtest-bascomp dvxtest-clean dvxtest-force dvxtest-hlpc
    +.PHONY: dvxtest-libs dvxtest-widgets dvxtest-apps dvxtest-bascomp dvxtest-clean dvxtest-force dvxtest-hlpc dvxtest-fonts
     
     # Host core, widget shared objects and the runner archive.
     dvxtest-libs: $(DVXTEST_ARCHIVES) dvxtest-widgets
    @@ -126,6 +135,10 @@ dvxtest-hlpc:
     dvxtest-bascomp:
     	$(MAKE) -C $(DVXTEST_BASICDIR) $(DVXTEST_BASCOMPREL)
     
    +# The fonts stamp stands for the whole set (src/tools/Makefile)
    +dvxtest-fonts:
    +	$(MAKE) -C $(DVXTEST_ROOT)/src/tools $(DVXTEST_FONTSREL) COV=0
    +
     $(DVXTEST_LIBDVX_A): dvxtest-force
     	$(MAKE) -C $(DVXTEST_LIBDVX) host SAN=$(SAN)
     
    diff --git a/src/test/dvxtest/golden/fontGlyphs32.png b/src/test/dvxtest/golden/fontGlyphs32.png
    new file mode 100644
    index 00000000..0d15f9b4
    --- /dev/null
    +++ b/src/test/dvxtest/golden/fontGlyphs32.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:b18ce62e95b263ccad8b38ee12538c4bca020b7cecc2e79362ca9e6fdb4564f7
    +size 4486
    diff --git a/src/test/dvxtest/golden/fontGlyphs8.png b/src/test/dvxtest/golden/fontGlyphs8.png
    new file mode 100644
    index 00000000..ab4883be
    --- /dev/null
    +++ b/src/test/dvxtest/golden/fontGlyphs8.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:6a0023143d7b3174f0cad0faf0d211f3873dc43ed2e89f37819f5e5f45271bf6
    +size 2929
    diff --git a/src/test/dvxtest/staticsAllowlist.txt b/src/test/dvxtest/staticsAllowlist.txt
    index c3e66aac..2478b113 100644
    --- a/src/test/dvxtest/staticsAllowlist.txt
    +++ b/src/test/dvxtest/staticsAllowlist.txt
    @@ -13,3 +13,6 @@ sWidgetGen
     # Per-allocation widget serial: must stay unique across resets for the
     # same reason.
     sWidgetSerial
    +# The font dialog's style names: a constant table the style list box
    +# takes as its items (wgtListBoxSetItems wants non-const pointers).
    +sFontStyles
    diff --git a/src/test/formrt/Makefile b/src/test/formrt/Makefile
    index fcc38db4..7fc79177 100644
    --- a/src/test/formrt/Makefile
    +++ b/src/test/formrt/Makefile
    @@ -79,8 +79,10 @@ $(TARGET): $(OBJS) $(STUBS_SO) $(DVXTEST_BASRT_A) $(DVXTEST_TASKS_A) $(DVXTEST_A
     -include $(OBJS:.o=.d) $(STUBS_SO:.so=.d)
     
     # Runs from its object directory so the log and scratch files it writes
    -# never land in the source tree.
    -run: $(TARGET) $(DVXTEST_HELP_HLP)
    +# never land in the source tree; the fonts are linked in there (the Font
    +# property cases draw with them).
    +run: $(TARGET) $(DVXTEST_HELP_HLP) dvxtest-fonts
    +	ln -sfn $(DVXTEST_FONTS) $(BINDIR)/$(DVXTEST_FONTS_DIR)
     	cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1 ./testFormRt
     
     clean:
    diff --git a/src/test/formrt/frtHarness.c b/src/test/formrt/frtHarness.c
    index 48b91aec..1185a52e 100644
    --- a/src/test/formrt/frtHarness.c
    +++ b/src/test/formrt/frtHarness.c
    @@ -152,6 +152,15 @@ static int32_t emitCtrl(const BasFormT *form, const BasControlT *ctrl, char *buf
             basValRelease(&v);
         }
     
    +    // The Font properties of a font of the control's own
    +    for (int32_t i = 0; w->font && basFontPropAt(i); i++) {
    +        const char *name = basFontPropAt(i);
    +        BasValueT   v    = basFormRtGetProp(rt, (void *)ctrl, name);
    +
    +        pos = emitPropValue(buf, bufSize, pos, indent + 1, name, basFormRtFindCommonProp(name)->type, v);
    +        basValRelease(&v);
    +    }
    +
         if (ctrl->iface && ctrl->iface->isContainer) {
             pos = emitSubtree(form, w, buf, bufSize, pos, indent + 1);
         }
    @@ -177,6 +186,12 @@ static int32_t emitFormHeader(const BasFormT *form, char *buf, int32_t bufSize,
                 continue;
             }
     
    +        // The Font properties only for a font of the form's own, as the
    +        // IDE designer keeps them
    +        if (basFontPropIs(pd->name) && !form->root->font) {
    +            continue;
    +        }
    +
             if (form->frmCentered && (strcasecmp(pd->name, "Left") == 0 || strcasecmp(pd->name, "Top") == 0)) {
                 continue;
             }
    diff --git a/src/test/formrt/frtHarness.h b/src/test/formrt/frtHarness.h
    index e0c357fa..ab4cac5f 100644
    --- a/src/test/formrt/frtHarness.h
    +++ b/src/test/formrt/frtHarness.h
    @@ -91,7 +91,9 @@ void        frtOutClear(FrtAppT *app);
     // Serialize form back to .frm text from the runtime's view.  Controls
     // are walked in widget-tree order; every interface property with a
     // getter and a setter, then the shared common properties, are written
    -// through the same descriptors the loader used.  Returns the length.
    +// through the same descriptors the loader used; the Font properties of
    +// the form and of each control that has a font of its own.  Returns the
    +// length.
     int32_t frtSerializeForm(const BasFormT *form, char *buf, int32_t bufSize);
     
     // True when a widget of this type stores text set through wgtSetText
    diff --git a/src/test/formrt/testFontProps.c b/src/test/formrt/testFontProps.c
    new file mode 100644
    index 00000000..6ae9a6c1
    --- /dev/null
    +++ b/src/test/formrt/testFontProps.c
    @@ -0,0 +1,971 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// testFontProps.c -- the Font properties (suite T5): FontName, FontSize,
    +// FontBold, FontItalic, FontUnderline and FontStrikethru on controls and
    +// forms (inheritance, sizes snapping, an unknown family, controls that
    +// edit by columns), Print / TextWidth / TextHeight and the access-key
    +// members in the object's font, the properties in .frm files (the loader
    +// in any order, the live form's model, the model's writer) and DlgFont.
    +// The fonts are DVX's own: the suite runs where its Makefile links
    +// bin/fonts in as FONTS.
    +
    +#include "frtHarness.h"
    +#include "../../apps/kpunch/dvxbasic/formrt/frmModel.h"
    +#include "dvxDraw.h"
    +#include "dvxPlat.h"
    +#include "dvxText.h"
    +
    +#include 
    +#include 
    +#include 
    +#include 
    +
    +#define FP_SCREEN_W      640
    +#define FP_SCREEN_H      480
    +#define FP_SCREEN_BPP    32
    +#define FP_SANS          "DejaVu Sans"
    +#define FP_SERIF         "DejaVu Serif"
    +#define FP_MONO          "DejaVu Sans Mono"
    +#define FP_UNKNOWN       "No Such Font"
    +#define FP_SIZE          14         // a size the DejaVu families have
    +#define FP_SIZE_SERIF    20
    +#define FP_SIZE_HUGE     1000       // past every family's largest size
    +#define FP_SIZE_BETWEEN  21         // between two sizes the DejaVu families have
    +#define FP_SIZES_MAX     64
    +#define FP_TEXT_MAX      512
    +#define FP_SRC_MAX       65536
    +#define FP_SPC_COLUMNS   3          // Print Spc(3)
    +#define FP_FONT_SCALE    2
    +#define FP_ALL_STYLES    (FONT_STYLE_FACE | FONT_STYLE_LINES)
    +#define FP_NOT_A_STYLE   0x0100     // a style bit no font has
    +#define FP_RICH_SIZE     20         // RichLabel's second paragraph (DejaVu Serif has it)
    +#define FP_RICH_WRAP_W   100        // a RichLabel Width that fits one of FP_RICH_WORDS' words, not both
    +#define FP_RICH_WORDS    "aaaaaaaaaa bbbbbbbbbb"
    +#define FP_STR(x)        FP_STR_(x) // a number define as a string literal (for BASIC source)
    +#define FP_STR_(x)       #x
    +#define FP_GOLDEN_FRAMES 4          // frames to lay out and paint the golden form
    +#define FP_LICENCE_MAX   16384      // a font's notice and licence texts
    +
    +// What the licence script saw: the licence window's text, whether the
    +// System font's note came up, and whether it has asked for the licence
    +typedef struct {
    +    char text[FP_LICENCE_MAX];
    +    bool systemNote;
    +    bool asked;
    +} FpLicenceT;
    +
    +// A font the dialog script picks, and whether it found the family
    +typedef struct {
    +    const char *family;
    +    int32_t     size;
    +    uint32_t    style;
    +    bool        picked;
    +    bool        busy;
    +} FpPickT;
    +
    +static BasValueT callMethod(FrtAppT *app, void *obj, const char *name, const char *arg);
    +static void      caseDlgFont(void);
    +static void      caseFontDrawing(void);
    +static void      caseFontFrm(void);
    +static void      caseFontGoldens(void);
    +static void      caseFontLicence(void);
    +static void      caseFontProps(void);
    +static void      caseRichEdit(void);
    +static void      caseRichLabel(void);
    +static bool      fontIs(const WidgetT *w, const char *family, int32_t size, uint32_t style);
    +static void      fpLicenceScript(AppContextT *ctx, WindowT *modal, void *userCtx);
    +static void      fpPickScript(AppContextT *ctx, WindowT *modal, void *userCtx);
    +void             frtFontCases(void);
    +static int32_t   largestSize(const char *family, uint32_t style);
    +static bool      nearestSize(const char *family, uint32_t style, int32_t asked, int32_t got);
    +static int32_t   openedSize(const char *family, int32_t size, uint32_t style);
    +static double    propNumber(FrtAppT *app, void *obj, const char *name);
    +static void      propText(FrtAppT *app, void *obj, const char *name, char *out, int32_t outSize);
    +
    +// BASIC entry points of commdlg.bas (formrt.c, reached through DECLARE LIBRARY)
    +int32_t     DlgFont(const char *title, const char *family, int32_t size, int32_t style, int32_t flags);
    +const char *DlgFontName(void);
    +int32_t     DlgFontSize(void);
    +int32_t     DlgFontStyle(void);
    +
    +
    +// A drawing member of obj called with one text argument.
    +static BasValueT callMethod(FrtAppT *app, void *obj, const char *name, const char *arg) {
    +    BasValueT text = basValStringFromC(arg);
    +    BasValueT out  = basFormRtCallMethod(app->rt, obj, name, &text, 1);
    +
    +    basValRelease(&text);
    +    return out;
    +}
    +
    +
    +// DlgFont from C and from a BASIC program with commdlg.bas: the dialog
    +// starts at the font given and gives the one chosen (DlgFontName$,
    +// DlgFontSize, DlgFontStyle), Cancel leaves the starting font, a
    +// fixed-pitch dialog offers no proportional family, and without a
    +// running program there is no dialog.
    +static void caseDlgFont(void) {
    +    tCase("DlgFont: the font dialog from C and BASIC");
    +    tInit(FP_SCREEN_W, FP_SCREEN_H, FP_SCREEN_BPP);
    +
    +    TEST_EQ(DlgFont("Font", FP_SANS, FP_SIZE, FONT_STYLE_BOLD, 0), 0);
    +    TEST_STR_EQ(DlgFontName(), FP_SANS);
    +
    +    const char *frm = "Begin Form Form1\n    Caption = \"Dlg\"\nEnd\n";
    +    FrtAppT     app;
    +    FpPickT     pick;
    +
    +    if (!frtBuild(&app, "", &frm, 1)) {
    +        printf("  compile error: %s\n", app.error);
    +        TEST_FAIL("build");
    +        tShutdown();
    +        return;
    +    }
    +
    +    frtStart(&app, NULL);
    +
    +    // OK on a font picked in the dialog
    +    memset(&pick, 0, sizeof(pick));
    +    pick.family = FP_SERIF;
    +    pick.size   = FP_SIZE_SERIF;
    +    pick.style  = FONT_STYLE_ITALIC | FONT_STYLE_UNDERLINE;
    +    tDialogScript(fpPickScript, &pick);
    +    TEST_EQ(DlgFont("Font", FP_SANS, FP_SIZE, FONT_STYLE_BOLD, 0), -1);
    +    tDialogScript(NULL, NULL);
    +    TEST_TRUE(pick.picked);
    +    TEST_STR_EQ(DlgFontName(), FP_SERIF);
    +    TEST_EQ(DlgFontSize(), openedSize(FP_SERIF, FP_SIZE_SERIF, FONT_STYLE_ITALIC));
    +    TEST_EQ(DlgFontStyle(), FONT_STYLE_ITALIC | FONT_STYLE_UNDERLINE);
    +
    +    // Only fixed-pitch families: DejaVu Sans is not offered (the script
    +    // cancels), and the starting font stays, flags beyond the four
    +    // styles dropped
    +    memset(&pick, 0, sizeof(pick));
    +    pick.family = FP_SANS;
    +    pick.size   = FP_SIZE;
    +    tDialogScript(fpPickScript, &pick);
    +    TEST_EQ(DlgFont("Font", FP_MONO, FP_SIZE, FP_ALL_STYLES | FP_NOT_A_STYLE, DVX_FONT_DLG_FIXED), 0);
    +    tDialogScript(NULL, NULL);
    +    TEST_TRUE(!pick.picked);
    +    TEST_STR_EQ(DlgFontName(), FP_MONO);
    +    TEST_EQ(DlgFontSize(), FP_SIZE);
    +    TEST_EQ(DlgFontStyle(), FP_ALL_STYLES);
    +    frtDestroy(&app);
    +
    +    // From BASIC through commdlg.bas (DECLARE LIBRARY "basrt")
    +    int32_t len = 0;
    +    char   *inc = platformReadFile(FRT_INCLUDE_DIR "/commdlg.bas", &len);
    +    char   *src = (char *)malloc(FP_SRC_MAX);
    +
    +    TEST_TRUE(inc != NULL && src != NULL);
    +
    +    if (inc && src) {
    +        snprintf(src, FP_SRC_MAX, "%s\n"
    +            "If DlgFont(\"Font\", \"" FP_SANS "\", %d, 0, DLG_FONT_FIXED) Then Print DlgFontName$(); \"|\"; DlgFontStyle()\n",
    +            inc, FP_SIZE);
    +
    +        if (frtBuild(&app, src, NULL, 0)) {
    +            memset(&pick, 0, sizeof(pick));
    +            pick.family = FP_MONO;
    +            pick.size   = FP_SIZE;
    +            pick.style  = FONT_STYLE_BOLD;
    +            tDialogScript(fpPickScript, &pick);
    +            basNativeHostSetLive(true);
    +            TEST_EQ(frtRunMain(&app), BAS_VM_HALTED);
    +            basNativeHostSetLive(false);
    +            tDialogScript(NULL, NULL);
    +            TEST_TRUE(pick.picked);
    +            TEST_STR_EQ(frtOut(&app), FP_MONO "|1 \n");
    +            frtDestroy(&app);
    +        } else {
    +            printf("  compile error: %s\n", app.error);
    +            TEST_FAIL("build with commdlg.bas");
    +        }
    +    }
    +
    +    free(inc);
    +    free(src);
    +    tShutdown();
    +}
    +
    +
    +// Print, TextWidth, TextHeight, TextWidthAccel and PrintAccel in the
    +// object's font: a PictureBox's own, a form's (before and after it has a
    +// drawing area); CurrentX moves by the text's width, a new line by the
    +// font's line height, Spc by its character width; FontScale multiplies.
    +static void caseFontDrawing(void) {
    +    tCase("Print / TextWidth / TextHeight / PrintAccel in the object's font");
    +    tInit(FP_SCREEN_W, FP_SCREEN_H, FP_SCREEN_BPP);
    +
    +    const char *frm =
    +        "Begin Form Form1\n"
    +        "    Caption = \"Draw\"\n"
    +        "    Begin PictureBox Pic1\n"
    +        "        Width = 300\n"
    +        "        Height = 120\n"
    +        "        FontName = \"" FP_SANS "\"\n"
    +        "        FontSize = 14\n"
    +        "    End\n"
    +        "End\n"
    +        "\n"
    +        "Sub Form1_Hello\n"
    +        "    Pic1.Cls\n"
    +        "    Pic1.Print \"Hello\";\n"
    +        "End Sub\n"
    +        "Sub Form1_NewLine\n"
    +        "    Pic1.Print\n"
    +        "End Sub\n"
    +        "Sub Form1_Spc\n"
    +        "    Pic1.Print Spc(3);\n"
    +        "End Sub\n"
    +        "Sub Form1_Accel\n"
    +        "    Pic1.CurrentX = 0\n"
    +        "    Pic1.PrintAccel \"&Go\"\n"
    +        "End Sub\n"
    +        "Sub Form1_Scaled\n"
    +        "    Pic1.FontScale = 2\n"
    +        "End Sub\n"
    +        "Sub Form1_FormFont\n"
    +        "    Me.FontName = \"" FP_SERIF "\"\n"
    +        "End Sub\n"
    +        "Sub Form1_FormPrint\n"
    +        "    Me.CurrentX = 0\n"
    +        "    Me.Print \"Hi\";\n"
    +        "End Sub\n";
    +    FrtAppT app;
    +
    +    if (!frtBuild(&app, "", &frm, 1)) {
    +        printf("  compile error: %s\n", app.error);
    +        TEST_FAIL("build");
    +        tShutdown();
    +        return;
    +    }
    +
    +    frtStart(&app, NULL);
    +
    +    BasFormT    *form = frtForm(&app, "Form1");
    +    BasControlT *pic  = form ? frtCtrl(form, "Pic1") : NULL;
    +
    +    TEST_TRUE(pic != NULL);
    +
    +    if (!pic) {
    +        frtDestroy(&app);
    +        tShutdown();
    +        return;
    +    }
    +
    +    const BitmapFontT *font = wgtFont(pic->widget);
    +    BasValueT          v;
    +
    +    TEST_TRUE(fontIs(pic->widget, FP_SANS, openedSize(FP_SANS, FP_SIZE, 0), 0));
    +    TEST_TRUE((font->flags & FONT_FLAG_PROPORTIONAL) != 0);
    +
    +    v = callMethod(&app, pic, "TextWidth", "Hello");
    +    TEST_EQ((int32_t)basValToNumber(v), textWidth(font, "Hello"));
    +    basValRelease(&v);
    +    v = callMethod(&app, pic, "TextHeight", "A");
    +    TEST_EQ((int32_t)basValToNumber(v), font->charHeight);
    +    basValRelease(&v);
    +    v = callMethod(&app, pic, "TextWidthAccel", "&File");
    +    TEST_EQ((int32_t)basValToNumber(v), textWidthAccel(font, "&File"));
    +    basValRelease(&v);
    +
    +    // Print moves CurrentX by the text's width, a new line goes down a
    +    // line of the font, Spc counts the font's character width
    +    TEST_TRUE(frtFire(&app, "Form1", "Hello"));
    +    TEST_EQ((int32_t)propNumber(&app, pic, "CurrentX"), textWidth(font, "Hello"));
    +    TEST_TRUE(frtFire(&app, "Form1", "NewLine"));
    +    TEST_EQ((int32_t)propNumber(&app, pic, "CurrentX"), 0);
    +    TEST_EQ((int32_t)propNumber(&app, pic, "CurrentY"), font->charHeight);
    +    TEST_TRUE(frtFire(&app, "Form1", "Spc"));
    +    TEST_EQ((int32_t)propNumber(&app, pic, "CurrentX"), FP_SPC_COLUMNS * font->charWidth);
    +    TEST_TRUE(frtFire(&app, "Form1", "Accel"));
    +    TEST_EQ((int32_t)propNumber(&app, pic, "CurrentX"), textWidthAccel(font, "&Go"));
    +
    +    // FontScale: the font's sizes times as large
    +    TEST_TRUE(frtFire(&app, "Form1", "Scaled"));
    +    v = callMethod(&app, pic, "TextWidth", "Hello");
    +    TEST_EQ((int32_t)basValToNumber(v), FP_FONT_SCALE * textWidth(font, "Hello"));
    +    basValRelease(&v);
    +    v = callMethod(&app, pic, "TextHeight", "A");
    +    TEST_EQ((int32_t)basValToNumber(v), FP_FONT_SCALE * font->charHeight);
    +    basValRelease(&v);
    +
    +    // A form measures in its font before it has a drawing area, and
    +    // prints in it after
    +    TEST_TRUE(frtFire(&app, "Form1", "FormFont"));
    +
    +    const BitmapFontT *serif = wgtFont(form->root);
    +
    +    TEST_TRUE(fontIs(form->root, FP_SERIF, openedSize(FP_SERIF, FONT_CHAR_HEIGHT, 0), 0));
    +    v = callMethod(&app, &form->formCtrl, "TextWidth", "Hi");
    +    TEST_EQ((int32_t)basValToNumber(v), textWidth(serif, "Hi"));
    +    basValRelease(&v);
    +    TEST_TRUE(frtFire(&app, "Form1", "FormPrint"));
    +    TEST_EQ((int32_t)propNumber(&app, &form->formCtrl, "CurrentX"), textWidth(serif, "Hi"));
    +
    +    // The PictureBox keeps its own font
    +    TEST_TRUE(fontIs(pic->widget, FP_SANS, openedSize(FP_SANS, FP_SIZE, 0), 0));
    +    TEST_TRUE(!app.rt->terminated);
    +    frtDestroy(&app);
    +    tShutdown();
    +}
    +
    +
    +// The Font properties in .frm files: the loader takes them in any order
    +// (one font per block, built on the font the block inherits), the live
    +// form's model (FormText$) keeps them only for an object with a font of
    +// its own, and the model writes them typed and reads them back the same.
    +static void caseFontFrm(void) {
    +    tCase(".frm: Font properties loaded in any order, the live model, the writer");
    +    tInit(FP_SCREEN_W, FP_SCREEN_H, FP_SCREEN_BPP);
    +
    +    const char *frm =
    +        "Begin Form Form1\n"
    +        "    Caption = \"Frm\"\n"
    +        "    FontSize = 20\n"
    +        "    FontName = \"" FP_SERIF "\"\n"
    +        "    FontItalic = True\n"
    +        "    Begin Label Lbl1\n"
    +        "        Caption = \"One\"\n"
    +        "        FontBold = True\n"
    +        "        FontSize = 14\n"
    +        "        FontName = \"" FP_SANS "\"\n"
    +        "    End\n"
    +        "    Begin Label Lbl2\n"
    +        "        Caption = \"Two\"\n"
    +        "    End\n"
    +        "    Begin TextBox Txt1\n"
    +        "        FontName = \"" FP_MONO "\"\n"
    +        "        FontItalic = False\n"
    +        "    End\n"
    +        "End\n";
    +    FrtAppT app;
    +
    +    if (!frtBuild(&app, "", &frm, 1)) {
    +        printf("  compile error: %s\n", app.error);
    +        TEST_FAIL("build");
    +        tShutdown();
    +        return;
    +    }
    +
    +    frtStart(&app, NULL);
    +
    +    BasFormT    *form = frtForm(&app, "Form1");
    +    BasControlT *lbl1 = form ? frtCtrl(form, "Lbl1") : NULL;
    +    BasControlT *lbl2 = form ? frtCtrl(form, "Lbl2") : NULL;
    +    BasControlT *txt1 = form ? frtCtrl(form, "Txt1") : NULL;
    +
    +    TEST_TRUE(lbl1 && lbl2 && txt1);
    +
    +    if (!lbl1 || !lbl2 || !txt1) {
    +        frtDestroy(&app);
    +        tShutdown();
    +        return;
    +    }
    +
    +    // FontSize before FontName still gives the family's size; a control
    +    // builds on the font it inherits (Lbl1 keeps the form's italic)
    +    TEST_TRUE(fontIs(form->root, FP_SERIF, openedSize(FP_SERIF, FP_SIZE_SERIF, FONT_STYLE_ITALIC), FONT_STYLE_ITALIC));
    +    TEST_TRUE(fontIs(lbl1->widget, FP_SANS, openedSize(FP_SANS, FP_SIZE, FONT_STYLE_BOLD | FONT_STYLE_ITALIC), FONT_STYLE_BOLD | FONT_STYLE_ITALIC));
    +    TEST_PTR_EQ(lbl2->widget->font, NULL);
    +    TEST_PTR_EQ(wgtFont(lbl2->widget), wgtFont(form->root));
    +
    +    // The TextBox inherits the system font (it edits by columns and the
    +    // form's is proportional): its FontName builds on that
    +    TEST_TRUE(fontIs(txt1->widget, FP_MONO, openedSize(FP_MONO, FONT_CHAR_HEIGHT, 0), 0));
    +
    +    // The live form's model: the form's and the two own fonts, all six
    +    // properties each, nothing for Lbl2
    +    FrmModelT *m    = frmModelFromForm(app.rt, form);
    +    int32_t    len  = 0;
    +    char      *text = m ? frmModelText(m, &len) : NULL;
    +    char       want[FP_TEXT_MAX];
    +
    +    frmModelFree(m);
    +    frtDestroy(&app);
    +    TEST_TRUE(text != NULL);
    +
    +    if (!text) {
    +        tShutdown();
    +        return;
    +    }
    +
    +    snprintf(want, sizeof(want), "    FontName = \"%s\"\n    FontSize = %" PRId32 "\n    FontItalic = True\n", FP_SERIF, openedSize(FP_SERIF, FP_SIZE_SERIF, FONT_STYLE_ITALIC));
    +    TEST_TRUE(strstr(text, want) != NULL);
    +    snprintf(want, sizeof(want), "        FontName = \"%s\"\n        FontSize = %" PRId32 "\n        FontBold = True\n        FontItalic = True\n        FontUnderline = False\n        FontStrikethru = False\n", FP_SANS, openedSize(FP_SANS, FP_SIZE, FONT_STYLE_BOLD | FONT_STYLE_ITALIC));
    +    TEST_TRUE(strstr(text, want) != NULL);
    +
    +    const char *two = strstr(text, "Begin Label Lbl2");
    +    const char *end = two ? strstr(two, "    End\n") : NULL;
    +
    +    TEST_TRUE(two && end && !memmem(two, (size_t)(end - two), "Font", strlen("Font")));
    +
    +    // The model reads its own text back the same
    +    FrmModelT *back     = frmModelLoad(text, len);
    +    int32_t    backLen  = 0;
    +    char      *backText = back ? frmModelText(back, &backLen) : NULL;
    +
    +    TEST_TRUE(backText && strcmp(backText, text) == 0);
    +    free(backText);
    +    frmModelFree(back);
    +
    +    // And the runtime makes the same fonts of it
    +    const char *again = text;
    +
    +    if (frtBuild(&app, "", &again, 1)) {
    +        frtStart(&app, NULL);
    +        form = frtForm(&app, "Form1");
    +        lbl1 = form ? frtCtrl(form, "Lbl1") : NULL;
    +        lbl2 = form ? frtCtrl(form, "Lbl2") : NULL;
    +        TEST_TRUE(form && lbl1 && lbl2);
    +        TEST_TRUE(form && fontIs(form->root, FP_SERIF, openedSize(FP_SERIF, FP_SIZE_SERIF, FONT_STYLE_ITALIC), FONT_STYLE_ITALIC));
    +        TEST_TRUE(lbl1 && fontIs(lbl1->widget, FP_SANS, openedSize(FP_SANS, FP_SIZE, FONT_STYLE_BOLD | FONT_STYLE_ITALIC), FONT_STYLE_BOLD | FONT_STYLE_ITALIC));
    +        TEST_TRUE(lbl2 && !lbl2->widget->font);
    +        frtDestroy(&app);
    +    } else {
    +        printf("  compile error: %s\n", app.error);
    +        TEST_FAIL("rebuild from the model's text");
    +    }
    +
    +    free(text);
    +    tShutdown();
    +}
    +
    +
    +// Real fonts drawn, pixel for pixel: labels in DejaVu Sans, DejaVu Serif
    +// bold, DejaVu Sans Mono and Pacifico at 32 bits per pixel (smoothed:
    +// the shipped smoothed sizes) and at 8 (1-bit glyphs)
    +static void caseFontGoldens(void) {
    +    static const int32_t bpps[] = { 32, 8 };
    +    const char *frm =
    +        "Begin Form Form1\n"
    +        "    Caption = \"Glyphs\"\n"
    +        "    Width = 300\n"
    +        "    Height = 150\n"
    +        "    Begin Label L1\n"
    +        "        Caption = \"The quick brown fox\"\n"
    +        "        FontName = \"" FP_SANS "\"\n"
    +        "        FontSize = 14\n"
    +        "    End\n"
    +        "    Begin Label L2\n"
    +        "        Caption = \"Bold serif heading\"\n"
    +        "        FontName = \"" FP_SERIF "\"\n"
    +        "        FontSize = 20\n"
    +        "        FontBold = True\n"
    +        "    End\n"
    +        "    Begin Label L3\n"
    +        "        Caption = \"int main(void) { }\"\n"
    +        "        FontName = \"" FP_MONO "\"\n"
    +        "        FontSize = 12\n"
    +        "    End\n"
    +        "    Begin Label L4\n"
    +        "        Caption = \"Pacifico script\"\n"
    +        "        FontName = \"Pacifico\"\n"
    +        "        FontSize = 24\n"
    +        "    End\n"
    +        "End\n";
    +
    +    for (int32_t i = 0; i < DVX_ARRAY_LEN(bpps); i++) {
    +        char name[FP_TEXT_MAX];
    +
    +        snprintf(name, sizeof(name), "fontGlyphs%ld", (long)bpps[i]);
    +        tCase(name);
    +        tInit(FP_SCREEN_W, FP_SCREEN_H, bpps[i]);
    +
    +        FrtAppT app;
    +
    +        if (!frtBuild(&app, "", &frm, 1)) {
    +            printf("  compile error: %s\n", app.error);
    +            TEST_FAIL("build");
    +            tShutdown();
    +            continue;
    +        }
    +
    +        frtStart(&app, NULL);
    +        tPump(FP_GOLDEN_FRAMES);
    +
    +        BasFormT *form = frtForm(&app, "Form1");
    +
    +        TEST_TRUE(form && form->window);
    +        TEST_EQ(dvxFontSmooth(), bpps[i] > 8);
    +
    +        if (form && form->window) {
    +            tWindowGolden(name, form->window);
    +        }
    +
    +        frtDestroy(&app);
    +        tShutdown();
    +    }
    +}
    +
    +
    +// The properties on controls and on the form from BASIC and from C:
    +// the system font by default, a control inheriting the form's font and a
    +// container's, a control's own font changed one part at a time, sizes
    +// snapping to the family's, an unknown family giving the system font,
    +// and a control that edits by columns refusing a proportional family.
    +// The font dialog's Licence button: the chosen font's notice and licence
    +// in a window of their own (the DejaVu notice and the Bitstream licence,
    +// one line end each), a note for the System font
    +static void caseFontLicence(void) {
    +    const char *frm = "Begin Form Form1\n    Caption = \"Dlg\"\nEnd\n";
    +    FrtAppT     app;
    +    FpLicenceT  lic;
    +
    +    tCase("DlgFont: Licence shows the chosen font's notice and licence");
    +    tInit(FP_SCREEN_W, FP_SCREEN_H, FP_SCREEN_BPP);
    +
    +    if (!frtBuild(&app, "", &frm, 1)) {
    +        printf("  compile error: %s\n", app.error);
    +        TEST_FAIL("build");
    +        tShutdown();
    +        return;
    +    }
    +
    +    frtStart(&app, NULL);
    +    memset(&lic, 0, sizeof(lic));
    +    tDialogScript(fpLicenceScript, &lic);
    +    TEST_EQ(DlgFont("Font", FP_SANS, FP_SIZE, FONT_STYLE_BOLD, 0), 0);
    +    TEST_TRUE(strstr(lic.text, "DejaVu Sans Bold, converted for DVX from DejaVuSans-Bold.ttf.") != NULL);
    +    TEST_TRUE(strstr(lic.text, "Bitstream Vera Fonts Copyright") != NULL);
    +    TEST_TRUE(strchr(lic.text, '\r') == NULL);
    +    TEST_TRUE(!lic.systemNote);
    +    memset(&lic, 0, sizeof(lic));
    +    TEST_EQ(DlgFont("Font", FONT_SYSTEM_FAMILY, FONT_CHAR_HEIGHT, 0, 0), 0);
    +    tDialogScript(NULL, NULL);
    +    TEST_TRUE(lic.systemNote);
    +    TEST_STR_EQ(lic.text, "");
    +    frtDestroy(&app);
    +    tShutdown();
    +}
    +
    +
    +static void caseFontProps(void) {
    +    tCase("FontName / FontSize / FontBold / FontItalic / FontUnderline / FontStrikethru");
    +    tInit(FP_SCREEN_W, FP_SCREEN_H, FP_SCREEN_BPP);
    +
    +    const char *frm =
    +        "Begin Form Form1\n"
    +        "    Caption = \"Fonts\"\n"
    +        "    Begin Label Lbl1\n"
    +        "        Caption = \"One\"\n"
    +        "    End\n"
    +        "    Begin TextBox Txt1\n"
    +        "    End\n"
    +        "    Begin Frame Fra1\n"
    +        "        Caption = \"Box\"\n"
    +        "        Begin Label Lbl2\n"
    +        "            Caption = \"Two\"\n"
    +        "        End\n"
    +        "    End\n"
    +        "End\n"
    +        "\n"
    +        "Sub Form1_Defaults\n"
    +        "    Print Lbl1.FontName; \"|\"; Lbl1.FontSize; Lbl1.FontBold; Lbl1.FontItalic; Lbl1.FontUnderline; Lbl1.FontStrikethru; \"|\"; Me.FontName; \"|\"; Me.FontSize\n"
    +        "End Sub\n"
    +        "Sub Form1_SetForm\n"
    +        "    Me.FontName = \"" FP_SANS "\"\n"
    +        "    Me.FontSize = 14\n"
    +        "End Sub\n"
    +        "Sub Form1_SetLabel\n"
    +        "    Lbl1.FontBold = True\n"
    +        "    Lbl1.FontItalic = True\n"
    +        "    Lbl1.FontUnderline = True\n"
    +        "    Lbl1.FontStrikethru = True\n"
    +        "End Sub\n"
    +        "Sub Form1_ClearLabel\n"
    +        "    Lbl1.FontItalic = False\n"
    +        "    Lbl1.FontUnderline = 0\n"
    +        "    Lbl1.FontStrikethru = False\n"
    +        "End Sub\n"
    +        "Sub Form1_Huge\n"
    +        "    Lbl1.FontSize = 1000\n"
    +        "End Sub\n"
    +        "Sub Form1_Between\n"
    +        "    Lbl1.FontSize = 21\n"
    +        "End Sub\n"
    +        "Sub Form1_Unknown\n"
    +        "    Lbl1.FontName = \"" FP_UNKNOWN "\"\n"
    +        "End Sub\n"
    +        "Sub Form1_Mono\n"
    +        "    Txt1.FontName = \"" FP_MONO "\"\n"
    +        "End Sub\n"
    +        "Sub Form1_Sans\n"
    +        "    Txt1.FontName = \"" FP_SANS "\"\n"
    +        "End Sub\n"
    +        "Sub Form1_Frame\n"
    +        "    Fra1.FontName = \"" FP_SERIF "\"\n"
    +        "    Print Lbl2.FontName; \"|\"; Fra1.FontName; \"|\"; Lbl1.FontBold\n"
    +        "End Sub\n";
    +    FrtAppT app;
    +    char    text[FP_TEXT_MAX];
    +
    +    if (!frtBuild(&app, "", &frm, 1)) {
    +        printf("  compile error: %s\n", app.error);
    +        TEST_FAIL("build");
    +        tShutdown();
    +        return;
    +    }
    +
    +    frtStart(&app, NULL);
    +
    +    BasFormT    *form = frtForm(&app, "Form1");
    +    BasControlT *lbl1 = form ? frtCtrl(form, "Lbl1") : NULL;
    +    BasControlT *lbl2 = form ? frtCtrl(form, "Lbl2") : NULL;
    +    BasControlT *txt1 = form ? frtCtrl(form, "Txt1") : NULL;
    +
    +    TEST_TRUE(lbl1 && lbl2 && txt1);
    +
    +    if (!lbl1 || !lbl2 || !txt1) {
    +        frtDestroy(&app);
    +        tShutdown();
    +        return;
    +    }
    +
    +    // The system font everywhere at first
    +    frtOutClear(&app);
    +    TEST_TRUE(frtFire(&app, "Form1", "Defaults"));
    +    snprintf(text, sizeof(text), "%s|%d False False False False |%s|%d \n", FONT_SYSTEM_FAMILY, FONT_CHAR_HEIGHT, FONT_SYSTEM_FAMILY, FONT_CHAR_HEIGHT);
    +    TEST_STR_EQ(frtOut(&app), text);
    +
    +    // The form's font reaches its controls, which have none of their own;
    +    // the TextBox, which edits by columns, draws in the system font
    +    int32_t size = openedSize(FP_SANS, FP_SIZE, 0);
    +
    +    TEST_TRUE(frtFire(&app, "Form1", "SetForm"));
    +    TEST_TRUE(fontIs(form->root, FP_SANS, size, 0));
    +    propText(&app, &form->formCtrl, BAS_PROP_FONTNAME, text, sizeof(text));
    +    TEST_STR_EQ(text, FP_SANS);
    +    TEST_EQ((int32_t)propNumber(&app, &form->formCtrl, BAS_PROP_FONTSIZE), size);
    +    propText(&app, lbl1, BAS_PROP_FONTNAME, text, sizeof(text));
    +    TEST_STR_EQ(text, FP_SANS);
    +    TEST_EQ((int32_t)propNumber(&app, lbl2, BAS_PROP_FONTSIZE), size);
    +    TEST_PTR_EQ(lbl1->widget->font, NULL);
    +    propText(&app, txt1, BAS_PROP_FONTNAME, text, sizeof(text));
    +    TEST_STR_EQ(text, FONT_SYSTEM_FAMILY);
    +
    +    // Each flag changes the control's own font, one part at a time
    +    TEST_TRUE(frtFire(&app, "Form1", "SetLabel"));
    +    TEST_TRUE(lbl1->widget->font != NULL);
    +    TEST_TRUE(fontIs(lbl1->widget, FP_SANS, openedSize(FP_SANS, FP_SIZE, FONT_STYLE_BOLD | FONT_STYLE_ITALIC), FP_ALL_STYLES));
    +    TEST_TRUE(propNumber(&app, lbl1, BAS_PROP_FONTSTRIKETHRU) != 0);
    +    TEST_TRUE(propNumber(&app, &form->formCtrl, BAS_PROP_FONTBOLD) == 0);
    +    TEST_TRUE(frtFire(&app, "Form1", "ClearLabel"));
    +    TEST_TRUE(fontIs(lbl1->widget, FP_SANS, openedSize(FP_SANS, FP_SIZE, FONT_STYLE_BOLD), FONT_STYLE_BOLD));
    +    TEST_TRUE(propNumber(&app, lbl1, BAS_PROP_FONTUNDERLINE) == 0);
    +
    +    // Sizes snap to the ones the family has in the style
    +    TEST_TRUE(frtFire(&app, "Form1", "Huge"));
    +    TEST_EQ((int32_t)propNumber(&app, lbl1, BAS_PROP_FONTSIZE), largestSize(FP_SANS, FONT_STYLE_BOLD));
    +    TEST_TRUE(frtFire(&app, "Form1", "Between"));
    +    TEST_TRUE(nearestSize(FP_SANS, FONT_STYLE_BOLD, FP_SIZE_BETWEEN, (int32_t)propNumber(&app, lbl1, BAS_PROP_FONTSIZE)));
    +
    +    // A family there is none of is the system font
    +    TEST_TRUE(frtFire(&app, "Form1", "Unknown"));
    +    propText(&app, lbl1, BAS_PROP_FONTNAME, text, sizeof(text));
    +    TEST_STR_EQ(text, FONT_SYSTEM_FAMILY);
    +
    +    // A fixed-pitch family on the TextBox shows; a proportional one is
    +    // the system font there
    +    TEST_TRUE(frtFire(&app, "Form1", "Mono"));
    +    propText(&app, txt1, BAS_PROP_FONTNAME, text, sizeof(text));
    +    TEST_STR_EQ(text, FP_MONO);
    +    TEST_TRUE(frtFire(&app, "Form1", "Sans"));
    +    propText(&app, txt1, BAS_PROP_FONTNAME, text, sizeof(text));
    +    TEST_STR_EQ(text, FONT_SYSTEM_FAMILY);
    +    TEST_TRUE(txt1->widget->font != NULL);
    +
    +    // A container's font reaches the controls inside it
    +    frtOutClear(&app);
    +    TEST_TRUE(frtFire(&app, "Form1", "Frame"));
    +    snprintf(text, sizeof(text), "%s|%s|True \n", FP_SERIF, FP_SERIF);
    +    TEST_STR_EQ(frtOut(&app), text);
    +
    +    // From C on the form object
    +    basFormRtSetProp(app.rt, &form->formCtrl, BAS_PROP_FONTBOLD, basValBool(true));
    +    TEST_TRUE(propNumber(&app, &form->formCtrl, BAS_PROP_FONTBOLD) != 0);
    +    TEST_TRUE((wgtFont(form->root)->style & FONT_STYLE_BOLD) != 0);
    +    TEST_TRUE(!app.rt->terminated);
    +    frtDestroy(&app);
    +    tShutdown();
    +}
    +
    +
    +// A RichEdit from BASIC: its text, a selection given a style, a color, a
    +// family and a size, read back; alignment and Undo; Find; the RTF
    +// holding the bold run
    +static void caseRichEdit(void) {
    +    tCase("RichEdit from BASIC: selection styles, alignment, undo, Find, TextRTF");
    +    tInit(FP_SCREEN_W, FP_SCREEN_H, FP_SCREEN_BPP);
    +
    +    const char *frm =
    +        "Begin Form Form1\n"
    +        "    Caption = \"Edit\"\n"
    +        "    Begin RichEdit Edit1\n"
    +        "    End\n"
    +        "End\n"
    +        "\n"
    +        "Sub Form1_Fill\n"
    +        "    Edit1.Text = \"Hello world\"\n"
    +        "    Edit1.SelStart = 6\n"
    +        "    Edit1.SelLength = 5\n"
    +        "    Edit1.SelBold = True\n"
    +        "    Edit1.SelColor = RGB(255, 0, 0)\n"
    +        "    Edit1.SelFontName = \"" FP_SERIF "\"\n"
    +        "    Edit1.SelFontSize = 20\n"
    +        "    IF Edit1.SelText = \"world\" AND Edit1.SelBold AND NOT Edit1.SelItalic THEN PRINT \"styled\"\n"
    +        "    IF Edit1.SelColor = RGB(255, 0, 0) AND Edit1.SelFontName = \"" FP_SERIF "\" THEN PRINT \"font\"\n"
    +        "    Edit1.SelStart = 0\n"
    +        "    Edit1.SelAlignment = 1\n"
    +        "    IF Edit1.SelAlignment = 1 AND Edit1.CanUndo THEN PRINT \"centered\"\n"
    +        "    Edit1.Undo\n"
    +        "    IF Edit1.SelAlignment = 0 AND Edit1.CanRedo THEN PRINT \"undone\"\n"
    +        "    IF Edit1.Find(\"WORLD\") = 6 THEN PRINT \"found\"\n"
    +        "    IF INSTR(Edit1.TextRTF, \"\\b\") > 0 THEN PRINT \"rtf\"\n"
    +        "End Sub\n";
    +    FrtAppT app;
    +
    +    if (!frtBuild(&app, "", &frm, 1)) {
    +        printf("  compile error: %s\n", app.error);
    +        TEST_FAIL("build");
    +        tShutdown();
    +        return;
    +    }
    +
    +    frtStart(&app, NULL);
    +    TEST_TRUE(frtFire(&app, "Form1", "Fill"));
    +    TEST_STR_EQ(frtOut(&app), "styled\nfont\ncentered\nundone\nfound\nrtf\n");
    +    frtDestroy(&app);
    +    tShutdown();
    +}
    +
    +
    +// A RichLabel built from BASIC: runs in the control's font, bold, another
    +// family and size, a centered paragraph and a line break; SetFont "", 0
    +// and SetColor -1 go back to the control's.  Its height is the lines'.
    +static void caseRichLabel(void) {
    +    tCase("RichLabel from BASIC: runs, fonts, paragraphs, Width wrapping");
    +    tInit(FP_SCREEN_W, FP_SCREEN_H, FP_SCREEN_BPP);
    +
    +    const char *frm =
    +        "Begin Form Form1\n"
    +        "    Caption = \"Rich\"\n"
    +        "    Begin RichLabel Rich1\n"
    +        "    End\n"
    +        "End\n"
    +        "\n"
    +        "Sub Form1_Fill\n"
    +        "    Rich1.AddText \"Plain \"\n"
    +        "    Rich1.SetStyle 1\n"
    +        "    Rich1.AddText \"bold\"\n"
    +        "    Rich1.SetStyle 0\n"
    +        "    Rich1.SetFont \"" FP_SERIF "\", " FP_STR(FP_RICH_SIZE) "\n"
    +        "    Rich1.SetColor RGB(255, 0, 0)\n"
    +        "    Rich1.AddParagraph 1\n"
    +        "    Rich1.AddText \"Big red\"\n"
    +        "    Rich1.AddLineBreak\n"
    +        "    Rich1.SetFont \"\", 0\n"
    +        "    Rich1.SetColor -1\n"
    +        "    Rich1.AddText \"back\"\n"
    +        "End Sub\n"
    +        "Sub Form1_Empty\n"
    +        "    Rich1.Clear\n"
    +        "End Sub\n"
    +        "Sub Form1_Wrap\n"
    +        "    Rich1.Width = " FP_STR(FP_RICH_WRAP_W) "\n"
    +        "    Rich1.AddText \"" FP_RICH_WORDS "\"\n"
    +        "End Sub\n";
    +    FrtAppT app;
    +
    +    if (!frtBuild(&app, "", &frm, 1)) {
    +        printf("  compile error: %s\n", app.error);
    +        TEST_FAIL("build");
    +        tShutdown();
    +        return;
    +    }
    +
    +    frtStart(&app, NULL);
    +
    +    BasFormT    *form = frtForm(&app, "Form1");
    +    BasControlT *rich = form ? frtCtrl(form, "Rich1") : NULL;
    +
    +    TEST_TRUE(rich != NULL);
    +
    +    if (!rich) {
    +        frtDestroy(&app);
    +        tShutdown();
    +        return;
    +    }
    +
    +    BitmapFontT       *serif = dvxFontOpen(FP_SERIF, FP_RICH_SIZE, 0);
    +    const BitmapFontT *own   = wgtFont(rich->widget);
    +
    +    TEST_TRUE(frtFire(&app, "Form1", "Fill"));
    +    tPump(1);
    +    TEST_EQ(rich->widget->calcMinH, 2 * own->charHeight + serif->charHeight);
    +    TEST_TRUE(frtFire(&app, "Form1", "Empty"));
    +    tPump(1);
    +    TEST_EQ(rich->widget->calcMinH, own->charHeight);
    +
    +    // Width wraps the text: one word to a line
    +    TEST_TRUE(frtFire(&app, "Form1", "Wrap"));
    +    tPump(1);
    +    TEST_EQ(rich->widget->calcMinH, 2 * own->charHeight);
    +    dvxFontClose(serif);
    +    frtDestroy(&app);
    +    tShutdown();
    +}
    +
    +
    +// The font w draws with is family at size with exactly style.
    +static bool fontIs(const WidgetT *w, const char *family, int32_t size, uint32_t style) {
    +    const BitmapFontT *font = wgtFont(w);
    +    bool               same = strcmp(dvxFontFamilyOf(font), family) == 0 && font->pixelSize == size && font->style == style;
    +
    +    if (!same) {
    +        printf("  font is %s %" PRId32 " style %" PRIu32 ", not %s %" PRId32 " style %" PRIu32 "\n", dvxFontFamilyOf(font), font->pixelSize, font->style, family, size, style);
    +    }
    +
    +    return same;
    +}
    +
    +
    +// Dialog script: in the font dialog, Licence once, then Cancel; in the
    +// licence window, keep its text and Close; at the System font's note, OK.
    +// Not guarded: the licence window runs inside the Licence click.
    +static void fpLicenceScript(AppContextT *ctx, WindowT *modal, void *userCtx) {
    +    FpLicenceT *lic  = (FpLicenceT *)userCtx;
    +    WidgetT    *root = modal ? modal->widgetRoot : NULL;
    +
    +    (void)ctx;
    +
    +    if (!root) {
    +        return;
    +    }
    +
    +    if (strcmp(modal->title, "Font Licence") == 0) {
    +        lic->systemNote = true;
    +        tDialogClickButton(modal, "OK");
    +    } else if (strstr(modal->title, " Licence")) {
    +        const char *text = wgtGetText(root->firstChild);
    +
    +        snprintf(lic->text, sizeof(lic->text), "%s", text ? text : "");
    +        tDialogClickButton(modal, "Close");
    +    } else if (!lic->asked) {
    +        lic->asked = true;
    +        tDialogClickButton(modal, "Licence...");
    +    } else {
    +        tDialogClickButton(modal, "Cancel");
    +    }
    +}
    +
    +
    +// Dialog script: pick the font of the FpPickT in the font dialog (once),
    +// or cancel when the dialog does not offer its family.
    +static void fpPickScript(AppContextT *ctx, WindowT *modal, void *userCtx) {
    +    FpPickT *pick = (FpPickT *)userCtx;
    +
    +    (void)ctx;
    +
    +    if (pick->busy) {
    +        return;
    +    }
    +
    +    pick->busy   = true;
    +    pick->picked = tFontDialogPick(modal, pick->family, pick->size, pick->style);
    +
    +    if (!pick->picked) {
    +        tDialogClickButton(modal, "Cancel");
    +    }
    +
    +    pick->busy = false;
    +}
    +
    +
    +void frtFontCases(void) {
    +    caseFontProps();
    +    caseFontDrawing();
    +    caseFontFrm();
    +    caseFontGoldens();
    +    caseDlgFont();
    +    caseFontLicence();
    +    caseRichLabel();
    +    caseRichEdit();
    +}
    +
    +
    +// The largest size family has in style.
    +static int32_t largestSize(const char *family, uint32_t style) {
    +    int32_t sizes[FP_SIZES_MAX];
    +    int32_t count = dvxFontSizes(family, style & FONT_STYLE_FACE, sizes, FP_SIZES_MAX);
    +
    +    return (count > 0) ? sizes[DVX_MIN(count, FP_SIZES_MAX) - 1] : 0;
    +}
    +
    +
    +// got is a size family has in style, and none is nearer asked.
    +static bool nearestSize(const char *family, uint32_t style, int32_t asked, int32_t got) {
    +    int32_t sizes[FP_SIZES_MAX];
    +    int32_t count = DVX_MIN(dvxFontSizes(family, style & FONT_STYLE_FACE, sizes, FP_SIZES_MAX), FP_SIZES_MAX);
    +    bool    has   = false;
    +
    +    for (int32_t i = 0; i < count; i++) {
    +        if (abs(sizes[i] - asked) < abs(got - asked)) {
    +            return false;
    +        }
    +
    +        has = has || sizes[i] == got;
    +    }
    +
    +    return has;
    +}
    +
    +
    +// The pixel size the font engine gives for family at size in style.
    +static int32_t openedSize(const char *family, int32_t size, uint32_t style) {
    +    BitmapFontT *font = dvxFontOpen(family, size, style);
    +    int32_t      got  = font ? font->pixelSize : 0;
    +
    +    dvxFontClose(font);
    +    return got;
    +}
    +
    +
    +static double propNumber(FrtAppT *app, void *obj, const char *name) {
    +    BasValueT v   = basFormRtGetProp(app->rt, obj, name);
    +    double    num = basValToNumber(v);
    +
    +    basValRelease(&v);
    +    return num;
    +}
    +
    +
    +static void propText(FrtAppT *app, void *obj, const char *name, char *out, int32_t outSize) {
    +    BasValueT   v = basFormRtGetProp(app->rt, obj, name);
    +    BasStringT *s = basValFormatString(v);
    +
    +    snprintf(out, outSize, "%s", s->data);
    +    basStringUnref(s);
    +    basValRelease(&v);
    +}
    diff --git a/src/test/formrt/testFormRt.c b/src/test/formrt/testFormRt.c
    index b9c136df..03624785 100644
    --- a/src/test/formrt/testFormRt.c
    +++ b/src/test/formrt/testFormRt.c
    @@ -47,6 +47,8 @@
     #include "dvxWgtP.h"
     #include "dvxVideo.h"
     #include "dvxWm.h"
    +#include "dvxText.h"
    +#include "richText.h"
     #include "../../libs/kpunch/serial/seclink/secLink.h"
     #include "../../libs/kpunch/hlpfile/hlpFormat.h"
     
    @@ -151,6 +153,8 @@
     #define SIZE_PERCENT_MAX    100    // SizePercent's cap
     
     // Widget-independent line prefix of every round-trip form.
    +#define RT_CAPTION_MAX 64         // a round-trip control's caption
    +#define RT_VALUE_MAX   256        // a round-trip property value (a RichEdit's RTF)
     #define RT_FORM_HEADER \
         "VERSION DVX 1.00\n" \
         "Begin Form Form1\n" \
    @@ -305,7 +309,8 @@ typedef struct {
     
     
     // Cases in their own files (testLibBas.c, testFrmModel.c, testUserCtl.c,
    -// testUcDesign.c)
    +// testUcDesign.c, testFontProps.c)
    +void frtFontCases(void);
     void frtFrmModelCases(void);
     void frtLibBasCases(void);
     void frtUcDesignCases(void);
    @@ -606,10 +611,11 @@ static BasValueT callIntM(FrtAppT *app, BasControlT *ctrl, const char *method, c
     
     
     // basdemo: the DATA demo runs twice (RESTORE rewinds), the Graphics menu
    -// opened twice keeps one form with its 4 controls, and the dynamic form
    -// gets its caption.
    +// opened twice keeps one form with its 4 controls, the dynamic form gets
    +// its caption, and the Fonts demo (opened twice) builds its RichLabel and
    +// leaves the sample alone when the Font dialog is cancelled.
     static void caseAppBasDemo(void) {
    -    tCase("app basdemo: DATA twice, Graphics twice, DynForm caption, Objects");
    +    tCase("app basdemo: DATA twice, Graphics twice, DynForm caption, Objects, Fonts");
         tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
     
         static const char stubs[] =
    @@ -626,6 +632,25 @@ static void caseAppBasDemo(void) {
             "END FUNCTION\n"
             "FUNCTION DlgIntInput(BYVAL t AS STRING, BYVAL p AS STRING, BYVAL d AS LONG, BYVAL a AS LONG, BYVAL b AS LONG) AS LONG\n"
             "    DlgIntInput = d\n"
    +        "END FUNCTION\n"
    +        "CONST FONT_BOLD = 1\n"
    +        "CONST FONT_ITALIC = 2\n"
    +        "CONST FONT_UNDERLINE = 4\n"
    +        "CONST FONT_STRIKE = 8\n"
    +        "FUNCTION FontFamilyCount() AS LONG\n"
    +        "    FontFamilyCount = 3\n"
    +        "END FUNCTION\n"
    +        "FUNCTION DlgFont(BYVAL t AS STRING, BYVAL f AS STRING, BYVAL s AS LONG, BYVAL st AS LONG, BYVAL fl AS LONG) AS LONG\n"
    +        "    DlgFont = False\n"
    +        "END FUNCTION\n"
    +        "FUNCTION DlgFontName$()\n"
    +        "    DlgFontName$ = \"\"\n"
    +        "END FUNCTION\n"
    +        "FUNCTION DlgFontSize() AS LONG\n"
    +        "    DlgFontSize = 0\n"
    +        "END FUNCTION\n"
    +        "FUNCTION DlgFontStyle() AS LONG\n"
    +        "    DlgFontStyle = 0\n"
             "END FUNCTION\n";
         static const char tests[] =
             "\n"
    @@ -643,6 +668,18 @@ static void caseAppBasDemo(void) {
             "SUB BasicDemo_B3\n"
             "    mnuObjects_Click\n"
             "    PRINT \"B3 ok\"\n"
    +        "END SUB\n"
    +        "SUB BasicDemo_B4\n"
    +        "    mnuFonts_Click\n"
    +        "    mnuFonts_Click\n"
    +        "    FontPick\n"
    +        "    FontEdit.SelStart = 0\n"
    +        "    FontEdit.SelLength = 6\n"
    +        "    FontBold\n"
    +        "    IF FontEdit.SelBold THEN PRINT \"bold\"\n"
    +        "    FontBold\n"
    +        "    IF NOT FontEdit.SelBold THEN PRINT \"plain\"\n"
    +        "    PRINT \"B4 ok\"\n"
             "END SUB\n";
         FrtAppT app;
     
    @@ -655,12 +692,14 @@ static void caseAppBasDemo(void) {
         TEST_TRUE(frtFire(&app, "BasicDemo", "B1"));
         TEST_TRUE(frtFire(&app, "BasicDemo", "B2"));
         TEST_TRUE(frtFire(&app, "BasicDemo", "B3"));
    -    TEST_STR_EQ(frtOut(&app), "B1 ok\nB2 ok\nB3 ok\n");
    +    TEST_TRUE(frtFire(&app, "BasicDemo", "B4"));
    +    TEST_STR_EQ(frtOut(&app), "B1 ok\nB2 ok\nB3 ok\nbold\nplain\nB4 ok\n");
         TEST_EQ(app.dialogsDismissed, 0);
     
         BasFormT *main = frtForm(&app, "BasicDemo");
         BasFormT *gfx  = frtForm(&app, "GraphicsForm");
         BasFormT *dyn  = frtForm(&app, "DynForm");
    +    BasFormT *fnt  = frtForm(&app, "FontForm");
     
         // Each DATA run prints the three colors and the re-read first one.
         const char *out    = (main && frtCtrl(main, "OutArea")) ? wgtGetText(frtCtrl(main, "OutArea")->widget) : NULL;
    @@ -683,6 +722,17 @@ static void caseAppBasDemo(void) {
         TEST_TRUE(dyn && dyn->window);
         TEST_STR_EQ((dyn && dyn->window) ? dyn->window->title : "", "Dynamic Form (built in code)");
     
    +    // The Fonts demo: one form of 11 controls, the RichLabel at least two
    +    // paragraphs tall; the cancelled Font dialog left the info line alone;
    +    // the Bold button turned the selection bold and back (above)
    +    BasControlT *rich = fnt ? frtCtrl(fnt, "FontRich") : NULL;
    +    BasControlT *info = fnt ? frtCtrl(fnt, "FontInfo") : NULL;
    +
    +    TEST_TRUE(fnt && fnt->window && rich && info);
    +    TEST_EQ(fnt ? (int32_t)arrlen(fnt->controls) : -1, 11);
    +    TEST_TRUE(rich && rich->widget->calcMinH >= 2 * FONT_CHAR_HEIGHT);
    +    TEST_STR_EQ(info ? wgtGetText(info->widget) : "", "3 font families are installed.");
    +
         frtDestroy(&app);
         tShutdown();
     }
    @@ -1173,6 +1223,17 @@ static void caseBuildProject(void) {
         prefsSetString(prefs, BAS_INI_SECTION_FORMS, "File0", "bad.frm");
         TEST_TRUE(!basBuildConcatProject(prefs, dir, &ps) && strstr(ps.error, "No Begin Form") != NULL);
         basBuildFreeProject(&ps);
    +
    +    // [Fonts]: the spec lists them all, in order
    +    BasBuildSpecT fromPrj;
    +
    +    memset(&fromPrj, 0, sizeof(fromPrj));
    +    prefsSetString(prefs, BAS_INI_SECTION_FONTS, "File0", "title.dfn");
    +    prefsSetString(prefs, BAS_INI_SECTION_FONTS, "File1", "body.dfn");
    +    basBuildSpecFromProject(prefs, dir, &fromPrj);
    +    TEST_EQ(fromPrj.fontCount, 2);
    +    TEST_STR_EQ(fromPrj.fonts[0], "title.dfn");
    +    TEST_STR_EQ(fromPrj.fonts[1], "body.dfn");
         prefsClose(prefs);
     
         // Directory helper and project open.
    @@ -1215,6 +1276,42 @@ static void caseBuildProject(void) {
         const char *failure = basBuildApp("frtBuildProj/out.app", &spec);
     
         TEST_TRUE(failure != NULL && strstr(failure, "Cannot read icon file") != NULL);
    +
    +    // The fonts a program carries go in as FONTn font resources (a real
    +    // font from the suite's FONTS folder, by its full name); a file that
    +    // is not a DVX font, a missing one, and too many fail the build
    +    char cwd[DVX_MAX_PATH];
    +    char font[DVX_MAX_PATH];
    +
    +    TEST_TRUE(getcwd(cwd, sizeof(cwd)) != NULL);
    +    snprintf(font, sizeof(font), "%s/%s/DJVSANS.DFN", cwd, DVX_FONTS_DIR);
    +    spec.iconPath  = NULL;
    +    spec.fonts[0]  = font;
    +    spec.fontCount = 1;
    +    failure        = basBuildApp("frtBuildProj/out.app", &spec);
    +    TEST_TRUE(failure == NULL);
    +
    +    DvxResHandleT         *res   = dvxResOpen("frtBuildProj/out.app");
    +    const DvxResDirEntryT *entry = res ? dvxResFind(res, "FONT0") : NULL;
    +    int32_t                len   = 0;
    +    char                  *bytes = platformReadFile(font, &len);
    +
    +    TEST_TRUE(entry && entry->type == DVX_RES_FONT && entry->size == (uint32_t)len);
    +    free(bytes);
    +
    +    if (res) {
    +        dvxResClose(res);
    +    }
    +
    +    spec.fonts[0] = "main.bas";
    +    failure       = basBuildApp("frtBuildProj/out.app", &spec);
    +    TEST_TRUE(failure != NULL && strstr(failure, "main.bas cannot be read") != NULL);
    +    spec.fonts[0] = "nosuch.dfn";
    +    failure       = basBuildApp("frtBuildProj/out.app", &spec);
    +    TEST_TRUE(failure != NULL && strstr(failure, "Cannot read font") != NULL);
    +    spec.fontCount = BAS_BUILD_FONTS_MAX + 1;
    +    failure        = basBuildApp("frtBuildProj/out.app", &spec);
    +    TEST_TRUE(failure != NULL && strstr(failure, "at most") != NULL);
         basModuleFree(module);
         unlink(selfPath);
     
    @@ -8095,7 +8192,7 @@ static void caseRoundTrip(void) {
     
             for (int32_t p = 0; p < iface->propCount; p++) {
                 const WgtPropDescT *pd = &iface->props[p];
    -            char                val[128];
    +            char                val[RT_VALUE_MAX];
     
                 if (!pd->getFn || !pd->setFn) {
                     continue;
    @@ -10221,7 +10318,50 @@ static void parseOnMenuProp(void *userData, const char *key, const char *value)
     
     
     static const char *roundTripValue(const char *basName, const WgtPropDescT *p, char *buf, int32_t bufSize) {
    -    (void)basName;
    +    // A RichEdit's selection is run-time state: an empty one at the start
    +    // reads the style and paragraph of plain text; its RTF is the empty
    +    // selection's, and the text's the caption it holds
    +    if (strcasecmp(basName, "RichEdit") == 0) {
    +        if (p->type == WGT_IFACE_BOOL) {
    +            return "False";
    +        }
    +
    +        if (strcasecmp(p->name, "SelColor") == 0) {
    +            return "-1";
    +        }
    +
    +        if (strcasecmp(p->name, "SelAlignment") == 0) {
    +            return "0";
    +        }
    +
    +        if (strcasecmp(p->name, "SelFontName") == 0) {
    +            return FONT_SYSTEM_FAMILY;
    +        }
    +
    +        if (strcasecmp(p->name, "SelFontSize") == 0) {
    +            snprintf(buf, bufSize, "%d", FONT_CHAR_HEIGHT);
    +            return buf;
    +        }
    +
    +        if (strcasecmp(p->name, "SelRTF") == 0 || strcasecmp(p->name, "TextRTF") == 0) {
    +            RichTextT rt;
    +            char      caption[RT_CAPTION_MAX];
    +
    +            snprintf(caption, sizeof(caption), "Hello %s", basName);
    +            richTextInit(&rt);
    +
    +            if (strcasecmp(p->name, "TextRTF") == 0) {
    +                richTextInsertPlain(&rt, 0, caption, (int32_t)strlen(caption), 0);
    +            }
    +
    +            char *rtf = richRtfWrite(&rt, FONT_SYSTEM_FAMILY, NULL);
    +
    +            snprintf(buf, bufSize, "%s", rtf ? rtf : "");
    +            free(rtf);
    +            richTextFree(&rt);
    +            return buf;
    +        }
    +    }
     
         // No row: a Data control without a database has none
         if (strcasecmp(p->name, "ListIndex") == 0 || strcasecmp(p->name, "SelectedRow") == 0 || strcasecmp(p->name, "AbsolutePosition") == 0) {
    @@ -10597,6 +10737,7 @@ int main(void) {
         caseDynamicControl();
         frtUserCtlCases();
         frtUcDesignCases();
    +    frtFontCases();
     
         return tSuiteEnd();
     }
    diff --git a/src/test/formrt/testLibBas.c b/src/test/formrt/testLibBas.c
    index 75ce0914..7d0218db 100644
    --- a/src/test/formrt/testLibBas.c
    +++ b/src/test/formrt/testLibBas.c
    @@ -23,12 +23,12 @@
     // testLibBas.c -- library include files from BASIC (suite T5, round 2)
     //
     // One source for the CONSTs: every CONST of comm.bas, conn.bas, xfer.bas,
    -// print.bas, listhelp.bas and security.bas that mirrors a C value is
    -// compared with that value by the compiled BASIC program itself, so an
    -// include file can never drift from its header.  listhelp.bas and
    -// security.bas are then called for real (the selection model, keyboard
    -// navigation, type-ahead and reorder rules; random bytes, the key
    -// exchange, the cipher, hashes, HMAC, HKDF and the compare).
    +// print.bas, listhelp.bas, security.bas, commdlg.bas and font.bas that
    +// mirrors a C value is compared with that value by the compiled BASIC
    +// program itself, so an include file can never drift from its header.
    +// listhelp.bas and security.bas are then called for real (the selection
    +// model, keyboard navigation, type-ahead and reorder rules; random bytes,
    +// the key exchange, the cipher, hashes, HMAC, HKDF and the compare).
     
     #include "frtHarness.h"
     #include "listHelp.h"
    @@ -207,6 +207,17 @@ static const LibConstT sSecConsts[] = {
         LIB_CONST("SEC_ERR_IO",        SEC_ERR_IO)
     };
     
    +static const LibConstT sCommDlgConsts[] = {
    +    LIB_CONST("DLG_FONT_FIXED", DVX_FONT_DLG_FIXED)
    +};
    +
    +static const LibConstT sFontConsts[] = {
    +    LIB_CONST("FONT_BOLD",      FONT_STYLE_BOLD),
    +    LIB_CONST("FONT_ITALIC",    FONT_STYLE_ITALIC),
    +    LIB_CONST("FONT_UNDERLINE", FONT_STYLE_UNDERLINE),
    +    LIB_CONST("FONT_STRIKE",    FONT_STYLE_STRIKE)
    +};
    +
     // One include file and its CONSTs
     typedef struct {
         const char      *file;
    @@ -215,12 +226,14 @@ typedef struct {
     } LibIncludeT;
     
     static const LibIncludeT sIncludes[] = {
    -    { "comm.bas",     sCommConsts,  DVX_ARRAY_LEN(sCommConsts)  },
    -    { "conn.bas",     sConnConsts,  DVX_ARRAY_LEN(sConnConsts)  },
    -    { "xfer.bas",     sXferConsts,  DVX_ARRAY_LEN(sXferConsts)  },
    -    { "print.bas",    sPrintConsts, DVX_ARRAY_LEN(sPrintConsts) },
    -    { "listhelp.bas", sListConsts,  DVX_ARRAY_LEN(sListConsts)  },
    -    { "security.bas", sSecConsts,   DVX_ARRAY_LEN(sSecConsts)   }
    +    { "comm.bas",     sCommConsts,    DVX_ARRAY_LEN(sCommConsts)    },
    +    { "conn.bas",     sConnConsts,    DVX_ARRAY_LEN(sConnConsts)    },
    +    { "xfer.bas",     sXferConsts,    DVX_ARRAY_LEN(sXferConsts)    },
    +    { "print.bas",    sPrintConsts,   DVX_ARRAY_LEN(sPrintConsts)   },
    +    { "listhelp.bas", sListConsts,    DVX_ARRAY_LEN(sListConsts)    },
    +    { "security.bas", sSecConsts,     DVX_ARRAY_LEN(sSecConsts)     },
    +    { "commdlg.bas",  sCommDlgConsts, DVX_ARRAY_LEN(sCommDlgConsts) },
    +    { "font.bas",     sFontConsts,    DVX_ARRAY_LEN(sFontConsts)    }
     };
     
     // comm.bas entry points (formrt.c, reached through DECLARE LIBRARY)
    diff --git a/src/test/fuzz/Makefile b/src/test/fuzz/Makefile
    index eff7c6a3..f676436f 100644
    --- a/src/test/fuzz/Makefile
    +++ b/src/test/fuzz/Makefile
    @@ -30,6 +30,7 @@
     # Harnesses (each fuzz.c's header says what it checks):
     #   fuzzBasCompile  BASIC compiler front end (source -> module -> run)
     #   fuzzBasModule   BASIC module deserializer + VM (runtime/serialize.c)
    +#   fuzzDfn         .DFN font reader (dvxFontFmt.c) and the font engine
     #   fuzzFrmModel    .frm form model parser / writer (formrt/frmModel.c)
     #   fuzzHlpc        help compiler (.dhs -> .hlp, src/tools/dvxhlpc.c)
     #   fuzzHlpFile     compiled help file reader (libs/kpunch/hlpfile)
    @@ -46,7 +47,7 @@
     #
     # run (runFuzz.sh) gives each fuzzer its seeds -- /seeds in this
     # directory plus seeds generated from tree files (sample programs, .frm,
    -# INI, help sources, images, the sample goldens' modules) -- a corpus
    +# INI, help sources, images, fonts, the sample goldens' modules) -- a corpus
     # and a work directory under obj/fuzz, runs FUZZ_JOBS fuzzers at a time
     # (niced), saves crashes under obj/fuzz/crashes// and prints the
     # command that reproduces each one.  It exits 1 when anything crashed.
    @@ -69,7 +70,7 @@ FUZZ_DIR     := $(CH_ROOT)/src/test/fuzz
     FUZZ_SECONDS ?= 60
     FUZZ_JOBS    ?= 3
     
    -FUZZERS ?= fuzzBasCompile fuzzBasModule fuzzFrmModel fuzzHlpc fuzzHlpFile fuzzImage fuzzPacket fuzzPrefs fuzzRes
    +FUZZERS ?= fuzzBasCompile fuzzBasModule fuzzDfn fuzzFrmModel fuzzHlpc fuzzHlpFile fuzzImage fuzzPacket fuzzPrefs fuzzRes
     
     # Product sources, from the product Makefiles' lists (clangHost.mk) or,
     # for single files no list names, by path.  The form model needs the
    @@ -89,6 +90,7 @@ FZ_HLPWALK   := $(FUZZ_DIR)/fuzzHlpWalk.c
     
     CH_SRCS_fuzzBasCompile := $(FUZZ_DIR)/fuzzBasCompile.c $(FZ_UTIL) $(FZ_COMMON) $(FZ_BAS)
     CH_SRCS_fuzzBasModule  := $(FUZZ_DIR)/fuzzBasModule.c $(FZ_COMMON) $(FZ_BAS)
    +CH_SRCS_fuzzDfn        := $(FUZZ_DIR)/fuzzDfn.c $(FZ_UTIL) $(CH_LIBDVX)
     CH_SRCS_fuzzFrmModel   := $(FUZZ_DIR)/fuzzFrmModel.c $(FZ_UTIL) $(CH_BAS_RT) $(CH_LIBDVX)
     CH_SRCS_fuzzHlpc       := $(FUZZ_DIR)/fuzzHlpc.c $(FZ_HLPWALK) $(FZ_UTIL) $(FZ_HLPFILE) $(CH_TOOL_HLPC)
     CH_SRCS_fuzzHlpFile    := $(FUZZ_DIR)/fuzzHlpFile.c $(FZ_HLPWALK) $(FZ_UTIL) $(FZ_HLPFILE) $(CH_BAS_SUPPORT)
    diff --git a/src/test/fuzz/dfn/seeds/noBitmaps.dfn b/src/test/fuzz/dfn/seeds/noBitmaps.dfn
    new file mode 100644
    index 00000000..b472841c
    Binary files /dev/null and b/src/test/fuzz/dfn/seeds/noBitmaps.dfn differ
    diff --git a/src/test/fuzz/fuzzDfn.c b/src/test/fuzz/fuzzDfn.c
    new file mode 100644
    index 00000000..0cbdeaf9
    --- /dev/null
    +++ b/src/test/fuzz/fuzzDfn.c
    @@ -0,0 +1,318 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +
    +// fuzzDfn.c -- libFuzzer harness: the .DFN font reader and the font engine
    +//
    +// Input: a .DFN font.  The reader opens it three ways -- in memory, as a
    +// whole scratch file, and as a font inside a file (after other bytes,
    +// the way a program carries one as a resource) -- and all three must
    +// agree on the header, every strike, and both texts.  A strike that
    +// loads must place every glyph's bitmap inside its block, and each
    +// glyph's last byte is read.  Then the engine gets it: the font is added
    +// for a program (dvxFontAddMemory, refused when damaged), the first
    +// FUZZ_DFN_SIZES_MAX of its sizes opened in each of sStyles, and every
    +// character measured and drawn partly off both edges of a small display
    +// (the input's length picks its depth), smoothing off and then on (which
    +// rebuilds the open fonts), the licence read through the font, and the
    +// program's fonts removed.
    +// Seeds: the .DFN files in the tree and the small ones the build makes
    +// (runFuzz.sh).
    +
    +#include "fuzzUtil.h"
    +
    +#include "dvxDraw.h"
    +#include "dvxFontFmt.h"
    +#include "dvxMem.h"
    +#include "dvxPlat.h"
    +#include "dvxText.h"
    +#include "dvxVideo.h"
    +
    +#include 
    +#include 
    +#include 
    +
    +#define FUZZ_DFN_APP_ID      7         // the program the font is added for
    +#define FUZZ_DFN_DEPTHS      3
    +#define FUZZ_DFN_BPP_PAL     8
    +#define FUZZ_DFN_BPP_HIGH    16
    +#define FUZZ_DFN_BPP_TRUE    32
    +#define FUZZ_DFN_SCREEN_W    64
    +#define FUZZ_DFN_SCREEN_H    48
    +#define FUZZ_DFN_EDGE        8         // how far past an edge the text starts
    +#define FUZZ_DFN_SIZES_MAX   4         // sizes of a font the engine opens
    +#define FUZZ_DFN_STYLES      4
    +#define FUZZ_DFN_FONTS_MAX   (FUZZ_DFN_SIZES_MAX * FUZZ_DFN_STYLES)
    +#define FUZZ_DFN_PREFIX      13        // bytes before the font in the "inside a file" copy
    +#define FUZZ_DFN_SUFFIX      5         // and after it
    +#define FUZZ_DFN_FILLER      0xA5
    +#define FUZZ_DFN_SOURCES     3
    +
    +// The ways the reader opens the input
    +typedef enum {
    +    FuzzDfnMemoryE,
    +    FuzzDfnFileE,
    +    FuzzDfnInsideE
    +} FuzzDfnSourceE;
    +
    +static const int32_t  sDepths[FUZZ_DFN_DEPTHS] = { FUZZ_DFN_BPP_PAL, FUZZ_DFN_BPP_HIGH, FUZZ_DFN_BPP_TRUE };
    +static const uint32_t sStyles[FUZZ_DFN_STYLES] = { 0, FONT_STYLE_BOLD, FONT_STYLE_ITALIC | FONT_STYLE_UNDERLINE, FONT_STYLE_FACE | FONT_STYLE_STRIKE };
    +
    +static int32_t  sAppId = FUZZ_DFN_APP_ID;
    +static char     sChars[DFN_CHAR_COUNT];             // every character but NUL
    +static DisplayT sDisplays[FUZZ_DFN_DEPTHS];
    +static BlitOpsT sOps[FUZZ_DFN_DEPTHS];
    +
    +// Function prototypes (alphabetical)
    +static void checkStrike(const DfnStrikeDataT *s);
    +static void compareStrikes(const DfnSourceT *srcs, const DfnFileT *files);
    +static void compareTexts(const DfnSourceT *srcs, const DfnFileT *files, DfnTextE which);
    +static void drawFont(const BitmapFontT *font, int32_t depth);
    +static void engineRun(const uint8_t *data, size_t size, const DfnFileT *f, int32_t depth);
    +static void fail(const char *what);
    +int         LLVMFuzzerInitialize(int *argc, char ***argv);
    +int         LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
    +
    +
    +// Every glyph's bitmap inside the strike's block, its last byte read
    +static void checkStrike(const DfnStrikeDataT *s) {
    +    int32_t bpp = (s->info.flags & DFN_STRIKE_GRAY2) ? FONT_GRAY_BPP : 1;
    +
    +    for (int32_t c = 0; c < DFN_CHAR_COUNT; c++) {
    +        uint32_t bytes = FONT_GLYPH_BYTES_BPP(&s->glyphs[c], bpp);
    +
    +        if (bytes == 0) {
    +            continue;
    +        }
    +
    +        if (s->bitmapAt[c] > s->info.bitmapBytes || bytes > s->info.bitmapBytes - s->bitmapAt[c]) {
    +            fail("a glyph's bitmap runs past its strike's block");
    +        }
    +
    +        volatile uint8_t last = s->bitmaps[s->bitmapAt[c] + bytes - 1];
    +
    +        (void)last;
    +    }
    +}
    +
    +
    +// Each strike loaded from every source: the same result and the same
    +// glyphs and bitmaps
    +static void compareStrikes(const DfnSourceT *srcs, const DfnFileT *files) {
    +    for (int32_t i = 0; i < files[FuzzDfnMemoryE].header.strikeCount; i++) {
    +        DfnStrikeDataT strikes[FUZZ_DFN_SOURCES];
    +        bool           ok[FUZZ_DFN_SOURCES];
    +
    +        for (int32_t k = 0; k < FUZZ_DFN_SOURCES; k++) {
    +            ok[k] = dfnLoadStrike(&srcs[k], &files[k], i, &strikes[k], NULL, 0);
    +        }
    +
    +        for (int32_t k = 1; k < FUZZ_DFN_SOURCES; k++) {
    +            if (ok[k] != ok[FuzzDfnMemoryE]) {
    +                fail("a strike loads from one source and not another");
    +            }
    +
    +            if (ok[k] && (memcmp(&strikes[k].info, &strikes[FuzzDfnMemoryE].info, sizeof(DfnStrikeT)) != 0 || memcmp(strikes[k].glyphs, strikes[FuzzDfnMemoryE].glyphs, sizeof(strikes[k].glyphs)) != 0 || memcmp(strikes[k].bitmapAt, strikes[FuzzDfnMemoryE].bitmapAt, sizeof(strikes[k].bitmapAt)) != 0 || (strikes[k].info.bitmapBytes > 0 && memcmp(strikes[k].bitmaps, strikes[FuzzDfnMemoryE].bitmaps, strikes[k].info.bitmapBytes) != 0))) {
    +                fail("a strike reads differently from one source than another");
    +            }
    +        }
    +
    +        if (ok[FuzzDfnMemoryE]) {
    +            checkStrike(&strikes[FuzzDfnMemoryE]);
    +        }
    +
    +        for (int32_t k = 0; k < FUZZ_DFN_SOURCES; k++) {
    +            dfnFreeStrike(&strikes[k]);
    +        }
    +    }
    +}
    +
    +
    +// A text read from every source: the same, or NULL from all
    +static void compareTexts(const DfnSourceT *srcs, const DfnFileT *files, DfnTextE which) {
    +    char *texts[FUZZ_DFN_SOURCES];
    +
    +    for (int32_t k = 0; k < FUZZ_DFN_SOURCES; k++) {
    +        texts[k] = dfnLoadText(&srcs[k], &files[k], which, NULL, 0);
    +    }
    +
    +    for (int32_t k = 1; k < FUZZ_DFN_SOURCES; k++) {
    +        if ((texts[k] == NULL) != (texts[FuzzDfnMemoryE] == NULL) || (texts[k] && strcmp(texts[k], texts[FuzzDfnMemoryE]) != 0)) {
    +            fail("a text reads differently from one source than another");
    +        }
    +    }
    +
    +    for (int32_t k = 0; k < FUZZ_DFN_SOURCES; k++) {
    +        free(texts[k]);
    +    }
    +}
    +
    +
    +// Every character measured, then drawn past the top left and the bottom
    +// right corners, plain and with the lines
    +static void drawFont(const BitmapFontT *font, int32_t depth) {
    +    DisplayT       *d     = &sDisplays[depth];
    +    const BlitOpsT *ops   = &sOps[depth];
    +    uint32_t        white = packColor(d, UINT8_MAX, UINT8_MAX, UINT8_MAX);
    +    uint32_t        black = packColor(d, 0, 0, 0);
    +
    +    if (textWidth(font, sChars) < 0) {
    +        fail("a text measured less than nothing");
    +    }
    +
    +    drawText(d, ops, font, -FUZZ_DFN_EDGE, -FUZZ_DFN_EDGE, sChars, white, black, true);
    +    drawTextStyled(d, ops, font, d->width - FUZZ_DFN_EDGE, d->height - FUZZ_DFN_EDGE, sChars, FONT_STYLE_LINES, white, black, false);
    +}
    +
    +
    +// The font added for a program and drawn in its sizes and styles (see the
    +// header), then the program's fonts removed
    +static void engineRun(const uint8_t *data, size_t size, const DfnFileT *f, int32_t depth) {
    +    BitmapFontT *fonts[FUZZ_DFN_FONTS_MAX];
    +    int32_t      count = 0;
    +
    +    dvxFontSetSmooth(false);
    +
    +    if (!dvxFontAddMemory(sAppId, data, (uint32_t)size, "fuzz font")) {
    +        return;
    +    }
    +
    +    int32_t sizes[FUZZ_DFN_SIZES_MAX];
    +    int32_t sizeCount = dvxFontSizes(f->header.family, 0, sizes, FUZZ_DFN_SIZES_MAX);
    +
    +    for (int32_t i = 0; i < sizeCount && i < FUZZ_DFN_SIZES_MAX; i++) {
    +        for (int32_t s = 0; s < FUZZ_DFN_STYLES; s++) {
    +            BitmapFontT *font = dvxFontOpen(f->header.family, sizes[i], sStyles[s]);
    +
    +            if (font) {
    +                fonts[count++] = font;
    +                drawFont(font, depth);
    +            }
    +        }
    +    }
    +
    +    dvxFontSetSmooth(true);
    +
    +    for (int32_t i = 0; i < count; i++) {
    +        drawFont(fonts[i], depth);
    +        free(dvxFontLicence(fonts[i]));
    +    }
    +
    +    dvxFontSetSmooth(false);
    +
    +    for (int32_t i = 0; i < count; i++) {
    +        dvxFontClose(fonts[i]);
    +    }
    +
    +    dvxFontRemoveOwner(sAppId);
    +}
    +
    +
    +static void fail(const char *what) {
    +    fprintf(stderr, "fuzzDfn: %s\n", what);
    +    abort();
    +}
    +
    +
    +// A display and its drawing calls per depth, made once by the host
    +// platform layer; the fonts are the program's
    +int LLVMFuzzerInitialize(int *argc, char ***argv) {
    +    (void)argc;
    +    (void)argv;
    +
    +    for (int32_t i = 0; i < FUZZ_DFN_DEPTHS; i++) {
    +        if (platformVideoInit(&sDisplays[i], FUZZ_DFN_SCREEN_W, FUZZ_DFN_SCREEN_H, sDepths[i]) != 0) {
    +            fprintf(stderr, "fuzzDfn: platformVideoInit failed for %ld bpp\n", (long)sDepths[i]);
    +            abort();
    +        }
    +
    +        drawInit(&sOps[i], &sDisplays[i]);
    +    }
    +
    +    for (int32_t c = 1; c < DFN_CHAR_COUNT; c++) {
    +        sChars[c - 1] = (char)c;
    +    }
    +
    +    dvxMemAppIdPtr = &sAppId;
    +    return 0;
    +}
    +
    +
    +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    +    if (size == 0 || size > INT32_MAX - FUZZ_DFN_PREFIX - FUZZ_DFN_SUFFIX) {
    +        return 0;
    +    }
    +
    +    char     path[FUZZ_UTIL_PATH_MAX];
    +    char     insidePath[FUZZ_UTIL_PATH_MAX];
    +    uint8_t *inside = (uint8_t *)malloc(size + FUZZ_DFN_PREFIX + FUZZ_DFN_SUFFIX);
    +
    +    if (!inside) {
    +        return 0;
    +    }
    +
    +    memset(inside, FUZZ_DFN_FILLER, size + FUZZ_DFN_PREFIX + FUZZ_DFN_SUFFIX);
    +    memcpy(inside + FUZZ_DFN_PREFIX, data, size);
    +    fuzzUtilTempPath(path, sizeof(path), ".dfn");
    +    fuzzUtilTempPath(insidePath, sizeof(insidePath), "-inside.bin");
    +
    +    bool written = fuzzUtilWriteFile(path, data, size) && fuzzUtilWriteFile(insidePath, inside, size + FUZZ_DFN_PREFIX + FUZZ_DFN_SUFFIX);
    +
    +    free(inside);
    +
    +    if (written) {
    +        DfnSourceT srcs[FUZZ_DFN_SOURCES];
    +        DfnFileT   files[FUZZ_DFN_SOURCES];
    +        bool       ok[FUZZ_DFN_SOURCES];
    +
    +        memset(srcs, 0, sizeof(srcs));
    +        srcs[FuzzDfnMemoryE].name   = "memory";
    +        srcs[FuzzDfnMemoryE].data   = data;
    +        srcs[FuzzDfnMemoryE].length = (uint32_t)size;
    +        dfnSourceFile(&srcs[FuzzDfnFileE], path);
    +        dfnSourceFile(&srcs[FuzzDfnInsideE], insidePath);
    +        srcs[FuzzDfnInsideE].base   = FUZZ_DFN_PREFIX;
    +        srcs[FuzzDfnInsideE].length = (uint32_t)size;
    +
    +        for (int32_t k = 0; k < FUZZ_DFN_SOURCES; k++) {
    +            ok[k] = dfnOpen(&srcs[k], &files[k], NULL, 0);
    +        }
    +
    +        for (int32_t k = 1; k < FUZZ_DFN_SOURCES; k++) {
    +            if (ok[k] != ok[FuzzDfnMemoryE] || (ok[k] && memcmp(&files[k], &files[FuzzDfnMemoryE], sizeof(DfnFileT)) != 0)) {
    +                fail("the font opens differently from one source than another");
    +            }
    +        }
    +
    +        if (ok[FuzzDfnMemoryE]) {
    +            compareStrikes(srcs, files);
    +            compareTexts(srcs, files, DfnTextNoticeE);
    +            compareTexts(srcs, files, DfnTextLicenceE);
    +        }
    +
    +        engineRun(data, size, &files[FuzzDfnMemoryE], (int32_t)(size % FUZZ_DFN_DEPTHS));
    +    }
    +
    +    remove(path);
    +    remove(insidePath);
    +    return 0;
    +}
    diff --git a/src/test/fuzz/runFuzz.sh b/src/test/fuzz/runFuzz.sh
    index fd4c6d21..c4d251c0 100755
    --- a/src/test/fuzz/runFuzz.sh
    +++ b/src/test/fuzz/runFuzz.sh
    @@ -160,6 +160,11 @@ genSeeds() {
                 (cd "$HELP_FIXTURE" && "$DVXHLPC" -o "$corpus/fixture.hlp" --quiet -i . main.dhs @inputs.txt > /dev/null)
                 "$DVXHLPC" -o "$corpus/mini.hlp" --quiet "$HERE/hlpc/seeds/mini.dhs" > /dev/null
                 ;;
    +        fuzzDfn)
    +            # The test fonts, and the built fonts small enough to seed
    +            copyTreeFiles "$corpus" '*.DFN' '*.dfn'
    +            find "$ROOT/bin/fonts" "$ROOT/obj/installer/fonts" -maxdepth 1 -name '*.DFN' -size -"$SEED_MAX_BYTES"c -exec cp -t "$corpus" {} + 2> /dev/null
    +            ;;
             fuzzImage)
                 copyTreeFiles "$corpus" '*.bmp' '*.png' '*.gif' '*.jpg'
                 ;;
    diff --git a/src/test/ide/Makefile b/src/test/ide/Makefile
    index 8302af82..192816a4 100644
    --- a/src/test/ide/Makefile
    +++ b/src/test/ide/Makefile
    @@ -128,7 +128,10 @@ $(STUBTARGET): $(STUBOBJS) $(DVXTEST_ARCHIVES) | dvxtest-libs
     
     -include $(sort $(OBJS:.o=.d) $(STUBOBJS:.o=.d))
     
    -run: $(TARGET) $(STUBTARGET) bascomp
    +# The fonts are linked into the working folder (the designer's Font row
    +# cases draw with them).
    +run: $(TARGET) $(STUBTARGET) bascomp dvxtest-fonts
    +	ln -sfn $(DVXTEST_FONTS) $(BINDIR)/$(DVXTEST_FONTS_DIR)
     	cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1 ./testIde
     	cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1 ./testStub
     	$(MAKE) cli
    diff --git a/src/test/ide/testIde.c b/src/test/ide/testIde.c
    index 0e7b9d43..55da0419 100644
    --- a/src/test/ide/testIde.c
    +++ b/src/test/ide/testIde.c
    @@ -43,10 +43,12 @@
     
     #include "ideMain.c"
     #include "ideStubs.h"
    +#include "dvxText.h"
     #include "treeView/treeView.h"
     #include "spinner/spinner.h"
     
     #include 
    +#include 
     #include 
     #include 
     #include 
    @@ -100,6 +102,12 @@
     #define PREFS_DESC_LINES       "one\ntwo"
     #define PREFS_DESC_PREFIX      "one"
     #define BUILD_MODE_RELEASE     1
    +#define FONT_FIXTURE           IDETEST_FIXTURE_DIR "/../../tools/golden/fonts/TINY.DFN"  // family Tiny
    +#define FONT_FILE              "tiny.dfn"     // the project's font, in its folder
    +#define FONT_NOT_A_FONT        "notfont.dfn"
    +#define FONT_SIZES             2              // the sizes Tiny has
    +#define FONT_RUN_APP_ID        7              // the IDE's id while a program runs
    +#define TITLE_ADD_FONT         "Add Font"
     #define BTN_YES                "Yes"
     #define BTN_NO                 "No"
     #define BTN_CANCEL             "Cancel"
    @@ -152,6 +160,7 @@ static void caseMakeExecutable(void);
     static void caseOpenEditDesignSave(void);
     static void caseOpenViaFileDialog(void);
     static void casePreferences(void);
    +static void caseProjectFonts(void);
     static void casePromptNewAndOpen(void);
     static void casePromptRecent(void);
     static void caseRecentPersistence(void);
    @@ -177,6 +186,7 @@ static int32_t lineOf(const char *text, const char *needle);
     static void lineText(const char *text, int32_t line, char *out, int32_t outSize);
     static int32_t listRowByName(WidgetT *list, const char *name);
     static void openViaRecent(void);
    +static void ppdFontStep(WindowT *modal);
     static void prefsCheckClamped(WindowT *modal);
     static void prefsFill(WindowT *modal);
     static void prefsFillBig(WindowT *modal);
    @@ -221,6 +231,8 @@ static int32_t        sHandlerLine      = 0;   // editor line of the handler bre
     static DialogPlanT    sExitPlan;
     static char           sBascompPath[DVX_MAX_PATH];
     static const char    *sRunInputText  = NULL;  // reply for an INPUT box during a scripted run
    +static DialogPlanT   *sFontPlan      = NULL;  // caseProjectFonts: the plan ppdFontStep steers
    +static int32_t        sFontStep      = 0;
     
     
     // A breakpoint inside the Click handler pauses the VM in the middle of
    @@ -616,6 +628,87 @@ static void casePreferences(void) {
     // New Project and Open Project both prompt for a dirty module before
     // anything else happens: Cancel aborts, Yes saves and continues into the
     // next dialog.
    +// The fonts a program carries: Project Properties adds one (a file that
    +// is not a DVX font refused), Make builds it in exactly as bascomp does,
    +// and a run from the IDE has it for the run alone
    +static void caseProjectFonts(void) {
    +    tCase("project fonts: Project Properties, Make, and a run from the IDE");
    +    tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    +    fixturePrepare();
    +
    +    char fontPath[DVX_MAX_PATH];
    +    char notFont[DVX_MAX_PATH];
    +    char idePath[DVX_MAX_PATH];
    +    char refPath[DVX_MAX_PATH];
    +    char cmd[CMD_BUF];
    +
    +    snprintf(fontPath, sizeof(fontPath), "%s" DVX_PATH_SEP FONT_FILE, sProjectDir);
    +    snprintf(notFont, sizeof(notFont), "%s" DVX_PATH_SEP FONT_NOT_A_FONT, sProjectDir);
    +    TEST_TRUE(platformCopyFile(FONT_FIXTURE, fontPath));
    +    TEST_TRUE(tWriteFile(notFont, "not a font", (int32_t)strlen("not a font")));
    +    ideStart(true);
    +    openViaRecent();
    +
    +    // Add... the font, Add... the file that is not one (an error), OK
    +    DialogPlanT plan;
    +
    +    dialogPlanInit(&plan, BTN_NO);
    +    plan.fileTitle  = TITLE_ADD_FONT;
    +    plan.fileDir    = sProjectDir;
    +    plan.fileName   = FONT_FILE;
    +    plan.fileButton = BTN_OPEN;
    +    plan.custom     = ppdFontStep;
    +    sFontPlan       = &plan;
    +    sFontStep       = 0;
    +    tDialogScript(dialogAnswer, &plan);
    +    ideMenu(CMD_PRJ_PROPS);
    +    tDialogScript(NULL, NULL);
    +    TEST_EQ(plan.errors, 1);
    +    TEST_EQ(prjFontCount(&sProject), 1);
    +    TEST_STR_EQ(prjFontPath(&sProject, 0), FONT_FILE);
    +    TEST_TRUE(prjSave(&sProject));
    +
    +    // Make: the font goes in, as bascomp puts it
    +    snprintf(idePath, sizeof(idePath), "%s" DVX_PATH_SEP IDE_APP_NAME, tTempDir());
    +    snprintf(refPath, sizeof(refPath), "%s" DVX_PATH_SEP REF_APP_NAME, tTempDir());
    +    dialogPlanInit(&plan, BTN_NO);
    +    plan.fileDir     = tTempDir();
    +    plan.fileName    = IDE_APP_NAME;
    +    plan.fileButton  = BTN_SAVE;
    +    plan.choiceIndex = BUILD_MODE_RELEASE;
    +    tDialogScript(dialogAnswer, &plan);
    +    ideMenu(CMD_MAKE_EXE);
    +    tDialogScript(NULL, NULL);
    +    TEST_EQ(plan.errors, 0);
    +    snprintf(cmd, sizeof(cmd), "%s %s -o %s -release > /dev/null", sBascompPath, sDbpPath, refPath);
    +    TEST_EQ(system(cmd), 0);
    +    TEST_TRUE(filesIdentical(idePath, refPath));
    +
    +    DvxResHandleT         *res   = dvxResOpen(idePath);
    +    const DvxResDirEntryT *entry = res ? dvxResFind(res, "FONT0") : NULL;
    +
    +    TEST_TRUE(entry && entry->type == DVX_RES_FONT);
    +
    +    if (res) {
    +        dvxResClose(res);
    +    }
    +
    +    // A run: the project's fonts are the IDE's while it lasts
    +    int32_t  app  = FONT_RUN_APP_ID;
    +    int32_t *prev = dvxMemAppIdPtr;
    +
    +    dvxMemAppIdPtr = &app;
    +    runFontsAdd(FONT_RUN_APP_ID);
    +    TEST_EQ(dvxFontSizes("Tiny", 0, NULL, 0), FONT_SIZES);
    +    dvxFontRemoveOwner(FONT_RUN_APP_ID);
    +    TEST_EQ(dvxFontSizes("Tiny", 0, NULL, 0), 0);
    +    dvxMemAppIdPtr = prev;
    +    sFontPlan      = NULL;
    +    ideExit();
    +    tShutdown();
    +}
    +
    +
     static void casePromptNewAndOpen(void) {
         tCase("New/Open Project with a dirty module prompts");
         tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    @@ -1359,6 +1452,23 @@ static void openViaRecent(void) {
     }
     
     
    +// Project Properties in caseProjectFonts: Add... the font, then Add...
    +// the file that is not a font, then OK
    +static void ppdFontStep(WindowT *modal) {
    +    if (strcmp(modal->title, "Project Properties") != 0) {
    +        TEST_STR_EQ(modal->title, "(expected dialog)");
    +        tDialogClickButton(modal, BTN_CANCEL);
    +        return;
    +    }
    +
    +    if (sFontStep == 1) {
    +        sFontPlan->fileName = FONT_NOT_A_FONT;
    +    }
    +
    +    TEST_TRUE(tDialogClickButton(modal, (sFontStep++ < 2) ? "Add..." : BTN_OK));
    +}
    +
    +
     static void prefsCheckClamped(WindowT *modal) {
         char expected[IDE_TAB_WIDTH_BUF];
     
    @@ -1766,6 +1876,7 @@ static void treeSelectFile(int32_t fileIdx) {
     #include "testIdeDebug.c"
     #include "testIdeRegress.c"
     #include "testIdeUserCtl.c"
    +#include "testIdeFont.c"
     
     
     int main(void) {
    @@ -1785,6 +1896,7 @@ int main(void) {
         caseRunClickAndRefusals();
         caseBreakInHandler();
         caseMakeExecutable();
    +    caseProjectFonts();
         casePreferences();
         caseFindReplace();
         caseRecentPersistence();
    @@ -1794,6 +1906,7 @@ int main(void) {
         runDebugCases();
         runRegressCases();
         runUserCtlCases();
    +    runFontCases();
     
         return tSuiteEnd();
     }
    diff --git a/src/test/ide/testIdeDesign.c b/src/test/ide/testIdeDesign.c
    index fee23251..e99175fc 100644
    --- a/src/test/ide/testIdeDesign.c
    +++ b/src/test/ide/testIdeDesign.c
    @@ -81,6 +81,8 @@
     #define DR_MIN_FRMS        30     // the tree holds at least this many forms
     #define DR_PATH_MAX        1024
     #define DR_WIN_SIZE        200    // design window of the round-trip case
    +#define DT_SETTLE_FRAMES   3      // frames for a toolbox resize to lay out (the WrapBox's height fixup included)
    +#define DT_SHORT_H         120    // a toolbox made this far above the screen's bottom must scroll
     
     
     
    @@ -118,6 +120,7 @@ static void caseProcSplit(void);
     static void casePropGrid(void);
     static void casePropTree(void);
     static void caseRecentAndRemove(void);
    +static void caseTbxFlow(void);
     static void caseWinCmds(void);
     static WidgetT *dcCtrlWidget(int32_t idx);
     static void dcFormClick(int32_t x, int32_t y);
    @@ -150,6 +153,7 @@ static void dpTreeAppend(WidgetT *parent, WidgetT *item);
     static WidgetT *dpTreeFind(WidgetT *parent, const char *prefix);
     static void dpTreeInsertFirst(WidgetT *parent, WidgetT *item);
     static void drCollectFrms(const char *dir, char ***out);
    +static int32_t dtRows(const WidgetT *wrap, int32_t *firstRow);
     static bool evtListed(const char *name);
     void runDesignCases(void);
     
    @@ -3005,6 +3009,70 @@ static void caseRecentAndRemove(void) {
     
     // Window-menu commands, closing and reopening auxiliary windows, and
     // the code-window reopen paths for modules and forms.
    +// Toolbox layout: it opens with at least TBX_COLS tools in its first row
    +// and every row showing, reflows to more rows when made narrower, and
    +// made near the screen's bottom stops there with a scroll bar, still
    +// TBX_COLS across.
    +static void caseTbxFlow(void) {
    +    tCase("toolbox flows its tools, reflows narrower, scrolls when short");
    +    tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    +    fixturePrepare();
    +    ideStart(true);
    +    openViaRecent();
    +    treeOpenFile(FILE_FORM);
    +    dcLayout();
    +    TEST_TRUE(sToolboxWin != NULL);
    +
    +    if (!sToolboxWin) {
    +        ideExit();
    +        tShutdown();
    +        return;
    +    }
    +
    +    WidgetT *wrap     = sToolboxWin->widgetRoot->firstChild;
    +    int32_t  firstRow = 0;
    +    int32_t  rows     = dtRows(wrap, &firstRow);
    +    int32_t  btnW     = 0;
    +    int32_t  minW     = 0;
    +    int32_t  minH     = 0;
    +
    +    for (const WidgetT *btn = wrap->firstChild; btn; btn = btn->nextSibling) {
    +        btnW = DVX_MAX(btnW, btn->calcMinW);
    +    }
    +
    +    // Every row showing, no room kept for a scroll bar: TBX_COLS of the
    +    // widest tool exactly, and no empty band below the last row
    +    TEST_TRUE(firstRow >= TBX_COLS);
    +    TEST_PTR_EQ(sToolboxWin->vScroll, NULL);
    +    TEST_TRUE(sToolboxWin->y + sToolboxWin->h < SCREEN_H);
    +    TEST_EQ(wrap->w, TBX_COLS * btnW + (TBX_COLS - 1) * wgtGetSpacingPx(wrap) + 2 * wgtGetPaddingPx(wrap));
    +    TEST_EQ(sToolboxWin->contentH, sToolboxWin->widgetRoot->calcMinH);
    +
    +    // As narrow as it goes: fewer across, more rows
    +    wmMinWindowSize(sToolboxWin, &minW, &minH);
    +    dvxResizeWindow(sAc, sToolboxWin, minW, sToolboxWin->h);
    +    tPump(DT_SETTLE_FRAMES);
    +    TEST_TRUE(dtRows(wrap, &firstRow) > rows);
    +    TEST_TRUE(firstRow < TBX_COLS);
    +
    +    // Made near the screen's bottom: cut there, scrolling, TBX_COLS across
    +    tbxDestroy(sAc, sToolboxWin);
    +    sToolboxWin = tbxCreate(sAc, &sDesigner, SCREEN_H - DT_SHORT_H);
    +    TEST_TRUE(sToolboxWin != NULL);
    +
    +    if (sToolboxWin) {
    +        tPump(DT_SETTLE_FRAMES);
    +        TEST_EQ(sToolboxWin->y + sToolboxWin->h, SCREEN_H);
    +        TEST_TRUE(sToolboxWin->vScroll != NULL);
    +        dtRows(sToolboxWin->widgetRoot->firstChild, &firstRow);
    +        TEST_TRUE(firstRow >= TBX_COLS);
    +    }
    +
    +    ideExit();
    +    tShutdown();
    +}
    +
    +
     static void caseWinCmds(void) {
         tCase("window commands, close/reopen paths, edit routing");
         tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    @@ -3256,13 +3324,11 @@ static int32_t dcToolsOutOfOrder(void) {
         char    prev[FRM_MAX_NAME] = "";
         int32_t bad                = 0;
     
    -    for (WidgetT *row = sToolboxWin->widgetRoot->firstChild; row; row = row->nextSibling) {
    -        for (WidgetT *btn = row->firstChild; btn; btn = btn->nextSibling) {
    -            btn->onClick(btn);
    -            bad += (strcasecmp(prev, sDesigner.activeTool) >= 0) ? 1 : 0;
    -            snprintf(prev, sizeof(prev), "%s", sDesigner.activeTool);
    -            btn->onClick(btn);
    -        }
    +    for (WidgetT *btn = sToolboxWin->widgetRoot->firstChild->firstChild; btn; btn = btn->nextSibling) {
    +        btn->onClick(btn);
    +        bad += (strcasecmp(prev, sDesigner.activeTool) >= 0) ? 1 : 0;
    +        snprintf(prev, sizeof(prev), "%s", sDesigner.activeTool);
    +        btn->onClick(btn);
         }
     
         return bad;
    @@ -4021,6 +4087,30 @@ static void drCollectFrms(const char *dir, char ***out) {
     
     // True when the Event dropdown lists event name (implemented, or
     // bracketed when it has no handler yet).
    +// The rows the toolbox's WrapBox laid its tools in (the tools of each
    +// row share a y); *firstRow the tools in the first
    +static int32_t dtRows(const WidgetT *wrap, int32_t *firstRow) {
    +    const WidgetT *prev = NULL;
    +    int32_t        rows = 0;
    +
    +    *firstRow = 0;
    +
    +    for (const WidgetT *btn = wrap->firstChild; btn; btn = btn->nextSibling) {
    +        if (!prev || btn->y != prev->y) {
    +            rows++;
    +        }
    +
    +        if (btn->y == wrap->firstChild->y) {
    +            (*firstRow)++;
    +        }
    +
    +        prev = btn;
    +    }
    +
    +    return rows;
    +}
    +
    +
     static bool evtListed(const char *name) {
         char bracketed[LINE_BUF];
     
    @@ -4061,6 +4151,7 @@ void runDesignCases(void) {
         caseGapData();
         caseGapMisc();
         caseGapTbxIcons();
    +    caseTbxFlow();
         casePrefsColors();
         caseHelpQuery();
         caseWinCmds();
    diff --git a/src/test/ide/testIdeFont.c b/src/test/ide/testIdeFont.c
    new file mode 100644
    index 00000000..d4bf7a18
    --- /dev/null
    +++ b/src/test/ide/testIdeFont.c
    @@ -0,0 +1,242 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// testIdeFont.c -- T6: the designer's Font row.  The row shows the font a
    +// control or the form draws with; the font dialog behind it stores the
    +// six Font properties (typed in the saved .frm), or removes them when the
    +// choice is the font the object has without them; the live widgets show
    +// the fonts (a control inherits the form's); a control that edits by
    +// columns gets a dialog of fixed-pitch families; the six rows edit the
    +// font too.  The fonts are DVX's own (the suite's Makefile links bin/fonts
    +// in as FONTS).
    +//
    +// Compiled as part of testIde.c (see the include there); every helper
    +// and the IDE's file-scope state are directly visible.
    +
    +#define DF_ROW_FONT        "Font"
    +#define DF_SANS            "DejaVu Sans"
    +#define DF_SERIF           "DejaVu Serif"
    +#define DF_MONO            "DejaVu Sans Mono"
    +#define DF_SIZE            14
    +#define DF_SIZE_SERIF      20
    +#define DF_SIZE_ROW        12     // typed into the FontSize row
    +#define DF_TEXT_MAX        128
    +#define DF_FONT_SAVE_BUF   65536  // .frm save buffer
    +#define DF_FORM_SELECTED   (-1)   // DsgnStateT.selectedIdx of the form itself (ideDesigner.c, private)
    +
    +// The font the dialog script picks, and whether the dialog offered it
    +static const char *sDfFamily = NULL;
    +static int32_t     sDfSize   = 0;
    +static uint32_t    sDfStyle  = 0;
    +static bool        sDfPicked = false;
    +
    +static void        caseFontRow(void);
    +static void        dfPick(WindowT *modal);
    +static void        dfPickFont(const char *family, int32_t size, uint32_t style);
    +static const char *dfRowValue(const char *rowName);
    +static int32_t     dfSize(const char *family, int32_t size, uint32_t style);
    +void               runFontCases(void);
    +
    +
    +// The Font row of a Label, a TextBox and the form.
    +static void caseFontRow(void) {
    +    tCase("designer: the Font row, the font dialog and the six Font properties");
    +    tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
    +    fixturePrepare();
    +    ideStart(true);
    +    openViaRecent();
    +    dcPrepareDesign();
    +    dcPlace(DC_TOOL_LABEL, DC_EMPTY_X, DC_PLACE_Y);
    +    dcPlace(DC_TOOL_TEXTBOX, DC_EMPTY_X, DC_PLACE_Y);
    +    dcLayout();
    +
    +    int32_t   count = (int32_t)arrlen(sDesigner.form->controls);
    +    FrmCtrlT *label = count >= 2 ? sDesigner.form->controls[count - 2] : NULL;
    +    FrmCtrlT *text  = count >= 2 ? sDesigner.form->controls[count - 1] : NULL;
    +    char      want[DF_TEXT_MAX];
    +
    +    TEST_TRUE(label && text && dsgnWidget(label) && dsgnWidget(text));
    +
    +    if (!label || !text || !dsgnWidget(label) || !dsgnWidget(text)) {
    +        ideExit();
    +        tShutdown();
    +        return;
    +    }
    +
    +    // The system font at first, with no Font properties stored
    +    dpSelectByPtr(label);
    +    snprintf(want, sizeof(want), "%s, %d", FONT_SYSTEM_FAMILY, FONT_CHAR_HEIGHT);
    +    TEST_STR_EQ(dfRowValue(DF_ROW_FONT), want);
    +
    +    // A font chosen in the dialog: all six properties, the live widget,
    +    // the row
    +    int32_t size = dfSize(DF_SANS, DF_SIZE, FONT_STYLE_BOLD);
    +
    +    dfPickFont(DF_SANS, DF_SIZE, FONT_STYLE_BOLD | FONT_STYLE_UNDERLINE);
    +    TEST_TRUE(sDfPicked);
    +    TEST_STR_EQ(frmCtrlGetProp(label, BAS_PROP_FONTNAME), DF_SANS);
    +    snprintf(want, sizeof(want), "%" PRId32, size);
    +    TEST_STR_EQ(frmCtrlGetProp(label, BAS_PROP_FONTSIZE), want);
    +    TEST_STR_EQ(frmCtrlGetProp(label, BAS_PROP_FONTBOLD), FRM_TEXT_TRUE);
    +    TEST_STR_EQ(frmCtrlGetProp(label, BAS_PROP_FONTITALIC), FRM_TEXT_FALSE);
    +    TEST_STR_EQ(frmCtrlGetProp(label, BAS_PROP_FONTUNDERLINE), FRM_TEXT_TRUE);
    +    TEST_STR_EQ(frmCtrlGetProp(label, BAS_PROP_FONTSTRIKETHRU), FRM_TEXT_FALSE);
    +    TEST_TRUE(dsgnWidget(label)->font != NULL);
    +    TEST_STR_EQ(dvxFontFamilyOf(wgtFont(dsgnWidget(label))), DF_SANS);
    +    TEST_EQ(wgtFont(dsgnWidget(label))->style, FONT_STYLE_BOLD | FONT_STYLE_UNDERLINE);
    +    snprintf(want, sizeof(want), "%s, %" PRId32 ", Bold, Underline", DF_SANS, size);
    +    TEST_STR_EQ(dfRowValue(DF_ROW_FONT), want);
    +    TEST_STR_EQ(dfRowValue(BAS_PROP_FONTNAME), DF_SANS);
    +    TEST_TRUE(sDesigner.form->dirty);
    +
    +    // Saved typed, and loaded back the same
    +    char      *buf  = (char *)malloc(DF_FONT_SAVE_BUF);
    +    int32_t    n    = buf ? dsgnSaveFrm(&sDesigner, buf, DF_FONT_SAVE_BUF) : -1;
    +    FrmModelT *back = (n > 0) ? frmModelLoad(buf, n) : NULL;
    +
    +    TEST_TRUE(n > 0 && strstr(buf, "        FontBold = True\n") != NULL);
    +    snprintf(want, sizeof(want), "        FontSize = %" PRId32 "\n", size);
    +    TEST_TRUE(n > 0 && strstr(buf, want) != NULL);
    +    TEST_TRUE(back != NULL);
    +
    +    if (back) {
    +        FrmCtrlT   *again = back->controls[frmModelFindCtrl(back, label->name, label->index)];
    +        const char *bold  = frmCtrlGetProp(again, BAS_PROP_FONTBOLD);
    +
    +        TEST_STR_EQ(bold ? bold : "", FRM_TEXT_TRUE);
    +        frmModelFree(back);
    +    }
    +
    +    free(buf);
    +
    +    // The row of one property edits the font too
    +    DialogPlanT plan;
    +
    +    dialogPlanInit(&plan, BTN_NO);
    +    plan.useSpinner = true;
    +    plan.intValue   = DF_SIZE_ROW;
    +    dpEditRow(BAS_PROP_FONTSIZE, &plan);
    +    TEST_EQ(wgtFont(dsgnWidget(label))->pixelSize, dfSize(DF_SANS, DF_SIZE_ROW, FONT_STYLE_BOLD));
    +
    +    // The font the label has without properties of its own: they go
    +    dfPickFont(FONT_SYSTEM_FAMILY, FONT_CHAR_HEIGHT, 0);
    +    TEST_TRUE(sDfPicked);
    +    TEST_TRUE(!frmCtrlGetProp(label, BAS_PROP_FONTNAME) && !frmCtrlGetProp(label, BAS_PROP_FONTBOLD));
    +    TEST_PTR_EQ(dsgnWidget(label)->font, NULL);
    +
    +    // The form's font: stored on the form, shown by the controls without
    +    // one of their own
    +    int32_t serif = dfSize(DF_SERIF, DF_SIZE_SERIF, FONT_STYLE_ITALIC);
    +
    +    sDesigner.selectedIdx = DF_FORM_SELECTED;
    +    prpRefresh(&sDesigner);
    +    snprintf(want, sizeof(want), "%s, %d", FONT_SYSTEM_FAMILY, FONT_CHAR_HEIGHT);
    +    TEST_STR_EQ(dfRowValue(DF_ROW_FONT), want);
    +    dfPickFont(DF_SERIF, DF_SIZE_SERIF, FONT_STYLE_ITALIC);
    +    TEST_TRUE(sDfPicked);
    +
    +    char value[FRM_MAX_TEXT];
    +
    +    TEST_TRUE(frmModelFormProp(sDesigner.form, BAS_PROP_FONTNAME, value, sizeof(value)));
    +    TEST_STR_EQ(value, DF_SERIF);
    +    TEST_TRUE(frmModelFormProp(sDesigner.form, BAS_PROP_FONTITALIC, value, sizeof(value)));
    +    TEST_STR_EQ(value, FRM_TEXT_TRUE);
    +    snprintf(want, sizeof(want), "%s, %" PRId32 ", Italic", DF_SERIF, serif);
    +    TEST_STR_EQ(dfRowValue(DF_ROW_FONT), want);
    +    dpSelectByPtr(label);
    +    TEST_STR_EQ(dfRowValue(DF_ROW_FONT), want);
    +    TEST_PTR_EQ(dsgnWidget(label)->font, NULL);
    +
    +    // The TextBox edits by columns: the system font in a proportional
    +    // form, and a dialog of fixed-pitch families only
    +    dpSelectByPtr(text);
    +    snprintf(want, sizeof(want), "%s, %d", FONT_SYSTEM_FAMILY, FONT_CHAR_HEIGHT);
    +    TEST_STR_EQ(dfRowValue(DF_ROW_FONT), want);
    +    dfPickFont(DF_SANS, DF_SIZE, 0);
    +    TEST_TRUE(!sDfPicked);
    +    TEST_TRUE(!frmCtrlGetProp(text, BAS_PROP_FONTNAME));
    +    dfPickFont(DF_MONO, DF_SIZE, 0);
    +    TEST_TRUE(sDfPicked);
    +    TEST_STR_EQ(frmCtrlGetProp(text, BAS_PROP_FONTNAME), DF_MONO);
    +    TEST_STR_EQ(dvxFontFamilyOf(wgtFont(dsgnWidget(text))), DF_MONO);
    +
    +    // The form's font back to the system font: its properties go, and
    +    // the label shows the system font again
    +    sDesigner.selectedIdx = DF_FORM_SELECTED;
    +    prpRefresh(&sDesigner);
    +    dfPickFont(FONT_SYSTEM_FAMILY, FONT_CHAR_HEIGHT, 0);
    +    TEST_TRUE(frmModelFormProp(sDesigner.form, BAS_PROP_FONTNAME, value, sizeof(value)));
    +    TEST_STR_EQ(value, "");
    +    TEST_STR_EQ(dvxFontFamilyOf(wgtFont(dsgnWidget(label))), FONT_SYSTEM_FAMILY);
    +
    +    ideExit();
    +    tShutdown();
    +}
    +
    +
    +// Font dialog script (DialogPlanT.custom): pick the requested font, or
    +// cancel when the dialog does not offer its family.
    +static void dfPick(WindowT *modal) {
    +    sDfPicked = tFontDialogPick(modal, sDfFamily, sDfSize, sDfStyle);
    +
    +    if (!sDfPicked) {
    +        tDialogClickButton(modal, BTN_CANCEL);
    +    }
    +}
    +
    +
    +// Double-click the Font row and pick family, size and style.
    +static void dfPickFont(const char *family, int32_t size, uint32_t style) {
    +    DialogPlanT plan;
    +
    +    sDfFamily = family;
    +    sDfSize   = size;
    +    sDfStyle  = style;
    +    sDfPicked = false;
    +    dialogPlanInit(&plan, BTN_NO);
    +    plan.custom = dfPick;
    +    dpEditRow(DF_ROW_FONT, &plan);
    +}
    +
    +
    +// The value of a row of the properties grid ("" when there is none).
    +static const char *dfRowValue(const char *rowName) {
    +    WidgetT *list = findOfClass(sPropsWin->widgetRoot, sListViewClass, 0);
    +    int32_t  row  = listRowByName(list, rowName);
    +
    +    return (row >= 0) ? wgtListViewGetCell(list, row, 1) : "";
    +}
    +
    +
    +// The pixel size the font engine gives for family at size in style.
    +static int32_t dfSize(const char *family, int32_t size, uint32_t style) {
    +    BitmapFontT *font = dvxFontOpen(family, size, style);
    +    int32_t      got  = font ? font->pixelSize : 0;
    +
    +    dvxFontClose(font);
    +    return got;
    +}
    +
    +
    +void runFontCases(void) {
    +    caseFontRow();
    +}
    diff --git a/src/test/lint/analyzerBaseline.txt b/src/test/lint/analyzerBaseline.txt
    index a60f02dd..e415344a 100644
    --- a/src/test/lint/analyzerBaseline.txt
    +++ b/src/test/lint/analyzerBaseline.txt
    @@ -30,14 +30,18 @@
     1 src/libs/kpunch/dvxshell/pmModel.c|pmHistoryAdd|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
     # FP: allocSlot appends the calloc'd slot to sApps with arrput; the stb_ds OOM hook never returns, so the slot is never dropped (freed with the slot index).
     1 src/libs/kpunch/dvxshell/shellApp.c|shellLoadAppInternal|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
    +# FP: dialogCreate stores &state->base (the first member) as the window's userData; dvxDialogRun frees it, and every caller (fnShowLicence included) runs the dialog it creates.
    +1 src/libs/kpunch/libdvx/dvxDialog.c|dvxDialogCreate|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
     # FP: name copy is stored in fd->entries[] and freed by the listing free loop.
     1 src/libs/kpunch/libdvx/dvxDialog.c|fdAddEntry|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
    -# FP: listItems malloc'd in fdLoadDir is freed by the free(fd->listItems) just above this NULL store; analyzer loses the pointer after the per-entry free loop.
    -1 src/libs/kpunch/libdvx/dvxDialog.c|fdFreeEntries|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
     # FP: fdAddEntry stores the grown realloc block in fd->entries before returning; the listing is freed by the dialog free loop.
     1 src/libs/kpunch/libdvx/dvxDialog.c|fdLoadDir|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
     # FP: copy replaces e->value (entry in h->entries, freed by prefsClose / freeEntry); new-key strings go into h->entries via arrins.
     2 src/libs/kpunch/libdvx/dvxPrefs.c|prefsSetString|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
    +# FP: idleTrim: oldest is set on every idle entry counted, and its callers pass keep >= 0 (FONT_IDLE_MAX or 0), so idle > keep means at least one idle entry and oldest is not NULL.
    +1 src/libs/kpunch/libdvx/dvxText.c|idleTrim|dereference of NULL  [CWE-476] [-Wanalyzer-null-dereference]
    +# FP: workArea gets width * height of a glyph whose width and height (uint8_t) are both positive on the paths that use the area, so the stb_ds array is allocated; the path assumes the area <= 0 and then enters the row loop.
    +1 src/libs/kpunch/libdvx/dvxText.c|workArea|use of NULL where non-null expected [CWE-476] [-Wanalyzer-null-argument]
     # FP: name is stored via arrput into the returned array (freed by dvxReadDirFree); was the stb_ds.h|dvxReadDir key.
     3 src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c|dvxReadDir|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
     # FP: same arrput store as ideProperties.c|addPropRow, reported at the stb_ds macro expansion.
    @@ -96,6 +100,8 @@
     2 src/widgets/kpunch/listView/widgetListView.c|wgtListViewAddItem.part.0|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
     # FP: strdup replaces lv->ownedCells[idx] (freed by the owned-cell free paths).
     1 src/widgets/kpunch/listView/widgetListView.c|wgtListViewSetCell.part.0|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
    +# FP: listViewBuildSortIndex stores the realloc'd sort index in lv->sortIndex before returning (freed by the next rebuild or destroy); the analyzer loses the pointer through the widget's data struct.
    +1 src/widgets/kpunch/listView/widgetListView.c|widgetListViewOnMouse|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
     # FP: the strdup'd mask is stored in ti->mask (freed by the next SetMask and with the widget); the failure path frees it.
     1 src/widgets/kpunch/textInput/widgetTextInput.c|wgtTextInputSetMask|leak of  [CWE-401] [-Wanalyzer-malloc-leak]
     # FP: item was validated non-NULL (VALIDATE_WIDGET_VOID) before the .part split; treeViewOf's loop test makes the analyzer assume NULL.
    diff --git a/src/test/lint/clangHost.mk b/src/test/lint/clangHost.mk
    index ab231268..17d4a6e5 100644
    --- a/src/test/lint/clangHost.mk
    +++ b/src/test/lint/clangHost.mk
    @@ -108,7 +108,7 @@ CH_DVXRES  := $(CH_ROOT)/bin/host/dvxres
     CH_BASSTUB := $(CH_ROOT)/obj/dvxbasic/basstub.app
     CH_NOICON  := $(CH_BASDIR)/noicon.bmp
     
    -CH_INCLUDES = -I$(CH_LIBDVXDIR) -I$(CH_LIBDVXDIR)/platform -I$(CH_LIBDVXDIR)/thirdparty -I$(CH_ROOT)/src/libs/kpunch/libtasks -I$(CH_ROOT)/src/libs/kpunch/texthelp -I$(CH_ROOT)/src/libs/kpunch/listhelp -I$(CH_ROOT)/src/libs/kpunch/hlpfile -I$(CH_ROOT)/src/libs/kpunch/dvxshell -I$(CH_ROOT)/src/widgets/kpunch -I$(CH_BASDIR) -I$(CH_TOOLSDIR)
    +CH_INCLUDES = -I$(CH_LIBDVXDIR) -I$(CH_LIBDVXDIR)/platform -I$(CH_LIBDVXDIR)/thirdparty -I$(CH_ROOT)/src/libs/kpunch/libtasks -I$(CH_ROOT)/src/libs/kpunch/texthelp -I$(CH_ROOT)/src/libs/kpunch/listhelp -I$(CH_ROOT)/src/libs/kpunch/richtext -I$(CH_ROOT)/src/libs/kpunch/hlpfile -I$(CH_ROOT)/src/libs/kpunch/dvxshell -I$(CH_ROOT)/src/widgets/kpunch -I$(CH_BASDIR) -I$(CH_TOOLSDIR)
     CH_CFLAGS   = $(CH_OPT) -fno-omit-frame-pointer -D_GNU_SOURCE -DDVX_HOST_TEST -w -MMD -MP $(CH_INCLUDES) $(CH_INSTR)
     
     chObjs = $(patsubst $(CH_ROOT)/%.c,$(CH_OBJ)/%.o,$1)
    diff --git a/src/test/lint/clangTidyBaseline.txt b/src/test/lint/clangTidyBaseline.txt
    index 2df8cdee..8ff32475 100644
    --- a/src/test/lint/clangTidyBaseline.txt
    +++ b/src/test/lint/clangTidyBaseline.txt
    @@ -80,10 +80,16 @@
     1 src/libs/kpunch/libdvx/dvxApp.c|bugprone-signed-char-misuse,cert-str34-c|'signed char' to 'int32_t' (aka 'int') conversion; consider casting to 'unsigned char' first.
     # FP: executeSysMenuCmd switches on the SysMenu*E ids openSysMenu put in the menu; handleMouseButton leaves the other HIT_* parts (none, border) without action on purpose
     2 src/libs/kpunch/libdvx/dvxApp.c|bugprone-switch-missing-default-case|switching on non-enum value without default case may not cover all cases
    +# FP: dvxFontDialog: the font list always holds the System family (scanFonts adds it first, fixed-pitch), so fn.families has at least one entry and current indexes it
    +1 src/libs/kpunch/libdvx/dvxDialog.c|clang-analyzer-core.NullDereference|Array access (via field 'families') results in a null pointer dereference
     # FP: the message box button id is kept in the button's userData on purpose
     1 src/libs/kpunch/libdvx/dvxDialog.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities
     # FP: realpath(resolved, canon): the path to resolve, then the output buffer, which is realpath's parameter order
     1 src/libs/kpunch/libdvx/dvxDialog.c|readability-suspicious-call-argument|1st argument 'resolved' (passed to '__name') looks like it might be swapped with the 2nd, 'canon' (passed to '__resolved')
    +# FP: workArea / synthItalic: the path assumes width * height <= 0 while also entering the row loop (height > 0) with width > 0; both are uint8_t glyph sizes, so the area is positive and the stb_ds array is allocated
    +1 src/libs/kpunch/libdvx/dvxText.c|clang-analyzer-core.NonNullParamChecker|Null pointer passed to 1st parameter expecting 'nonnull'
    +# FP: synthItalic copies from b->px, which levelsFrom sized width * height; the path assumes that area <= 0 while width and height (uint8_t) are both positive
    +1 src/libs/kpunch/libdvx/dvxText.c|clang-analyzer-core.NonNullParamChecker|Null pointer passed to 2nd parameter expecting 'nonnull'
     # FP: GNU ld --wrap=abort (HOST_APP_LDFLAGS) requires exactly the name __wrap_abort
     1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp|declaration uses identifier '__wrap_abort', which is a reserved identifier
     # FP: GNU ld --wrap=exit (HOST_APP_LDFLAGS) requires exactly the name __wrap_exit
    diff --git a/src/test/lint/codespellWords.txt b/src/test/lint/codespellWords.txt
    index 5cde8948..4fec884d 100644
    --- a/src/test/lint/codespellWords.txt
    +++ b/src/test/lint/codespellWords.txt
    @@ -44,3 +44,10 @@ keep-alives
     # search tests).
     hel
     alph
    +#
    +# RTF control words and destinations (\pard, \headerr) in the RTF
    +# reader, writer, tests and docs, and the test text "Caf\'e9" (an e
    +# with an acute accent).
    +pard
    +headerr
    +caf
    diff --git a/src/test/lint/cppcheckBaseline.txt b/src/test/lint/cppcheckBaseline.txt
    index 8ec01da1..5faefb4e 100644
    --- a/src/test/lint/cppcheckBaseline.txt
    +++ b/src/test/lint/cppcheckBaseline.txt
    @@ -188,6 +188,14 @@
     1 src/libs/kpunch/libdvx/dvxApp.c|constVariable|Variable 'keyRec' can be declared as const array
     # FP: style suggestion only (could be const), no defect.
     1 src/libs/kpunch/libdvx/dvxApp.c|constVariable|Variable 'mouseRec' can be declared as const array
    +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
    +1 src/libs/kpunch/libdvx/dvxFlow.c|unsignedLessThanZero|Checking if unsigned expression 'flow->items?((struct stbds_array_header*)(flow->items)-1)->capacity:0' is less than zero.
    +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
    +1 src/libs/kpunch/libdvx/dvxFlow.c|unsignedLessThanZero|Checking if unsigned expression 'flow->lines?((struct stbds_array_header*)(flow->lines)-1)->capacity:0' is less than zero.
    +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
    +1 src/libs/kpunch/libdvx/dvxFlow.c|unsignedLessThanZero|Checking if unsigned expression 'flow->segs?((struct stbds_array_header*)(flow->segs)-1)->capacity:0' is less than zero.
    +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
    +1 src/libs/kpunch/libdvx/dvxText.c|unsignedLessThanZero|Checking if unsigned expression '(sView)?((struct stbds_array_header*)(sView)-1)->capacity:0' is less than zero.
     # FP: the gate defines DXE_EXPORT as empty, which leaves DXE_EXPORT(strerror) as a bare (strerror) cppcheck cannot parse; the real macro builds the export table entry.
     1 src/libs/kpunch/libdvx/platform/dvxPlatformDos.c|unknownMacro|There is an unknown macro here somewhere. Configuration is required. If strerror is a macro then please configure it.
     # FP: raise() returns when no handler longjmps out (DOS without a recovery point); the abort() after it is the intended fallback.
    @@ -220,6 +228,20 @@
     1 src/libs/kpunch/print/printUi.c|variableScope|The scope of the variable 'allText' can be reduced.
     # FP: style suggestion only (variable scope), no defect.
     1 src/libs/kpunch/print/printUi.c|variableScope|The scope of the variable 'msg' can be reduced.
    +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
    +1 src/libs/kpunch/richtext/richRtf.c|unsignedLessThanZero|Checking if unsigned expression 'out.runs?((struct stbds_array_header*)(out.runs)-1)->capacity:0' is less than zero.
    +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
    +1 src/libs/kpunch/richtext/richText.c|unsignedLessThanZero|Checking if unsigned expression 'dst->aligns?((struct stbds_array_header*)(dst->aligns)-1)->capacity:0' is less than zero.
    +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
    +1 src/libs/kpunch/richtext/richText.c|unsignedLessThanZero|Checking if unsigned expression 'dst->runs?((struct stbds_array_header*)(dst->runs)-1)->capacity:0' is less than zero.
    +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
    +1 src/libs/kpunch/richtext/richText.c|unsignedLessThanZero|Checking if unsigned expression 'rt->aligns?((struct stbds_array_header*)(rt->aligns)-1)->capacity:0' is less than zero.
    +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
    +1 src/libs/kpunch/richtext/richText.c|unsignedLessThanZero|Checking if unsigned expression 'rt->runs?((struct stbds_array_header*)(rt->runs)-1)->capacity:0' is less than zero.
    +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
    +1 src/libs/kpunch/richtext/richText.c|unsignedLessThanZero|Checking if unsigned expression 'rt->styles?((struct stbds_array_header*)(rt->styles)-1)->capacity:0' is less than zero.
    +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
    +1 src/libs/kpunch/richtext/richText.c|unsignedLessThanZero|Checking if unsigned expression 'rt->text?((struct stbds_array_header*)(rt->text)-1)->capacity:0' is less than zero.
     # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const.
     1 src/libs/kpunch/serial/packet/packet.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'portRead' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s).
     # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const.
    @@ -248,6 +270,8 @@
     1 src/tools/proxy/proxy.c|unreadVariable|Variable 'fds[1].events' is assigned a value that is never used.
     # FP: style suggestion only (variable scope), no defect.
     1 src/tools/proxy/proxy.c|variableScope|The scope of the variable 'envPass' can be reduced.
    +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
    +1 src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c|unsignedLessThanZero|Checking if unsigned expression 'at->music?((struct stbds_array_header*)(at->music)-1)->capacity:0' is less than zero.
     # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const.
     1 src/widgets/kpunch/canvas/widgetCanvas.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'wgtCanvasSave' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s).
     # FP: re-entrancy guard: a user callback run just before can destroy widgets and bump sWidgetGen; cppcheck cannot see callbacks change the global.
    @@ -268,6 +292,16 @@
     1 src/widgets/kpunch/listView/widgetListView.c|variableScope|The scope of the variable 'textY' can be reduced.
     # FP: right == left on the first and last rows of the diamond (dist 0), where no fill line is drawn.
     1 src/widgets/kpunch/radio/widgetRadio.c|knownConditionTrueFalse|Condition 'right>left' is always true
    +# FP: wgtRichEditCopy is the RichEditApiT copy slot and the BASIC Copy method; both callback types take a WidgetT *, so the parameter cannot be const.
    +1 src/widgets/kpunch/richEdit/widgetRichEdit.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'wgtRichEditCopy' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s).
    +# FP: wgtRichEditSaveFile is the RichEditApiT saveFile slot and the BASIC SaveFile method; both callback types take a WidgetT *, so the parameter cannot be const.
    +1 src/widgets/kpunch/richEdit/widgetRichEdit.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'wgtRichEditSaveFile' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s).
    +# FP: widgetRichEditOnDragEnd is the class's WGT_METHOD_ON_DRAG_END handler; the handler type takes a WidgetT *, so the parameter cannot be const.
    +1 src/widgets/kpunch/richEdit/widgetRichEdit.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'widgetRichEditOnDragEnd' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s).
    +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
    +1 src/widgets/kpunch/richEdit/widgetRichEdit.c|unsignedLessThanZero|Checking if unsigned expression '(*stack)?((struct stbds_array_header*)(*stack)-1)->capacity:0' is less than zero.
    +# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
    +1 src/widgets/kpunch/richEdit/widgetRichEdit.c|unsignedLessThanZero|Checking if unsigned expression 'ed->fonts?((struct stbds_array_header*)(ed->fonts)-1)->capacity:0' is less than zero.
     # FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const.
     1 src/widgets/kpunch/textInput/widgetTextInput.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'wgtTextAreaCopy' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s).
     # FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
    diff --git a/src/test/lint/staticGate.sh b/src/test/lint/staticGate.sh
    index 3b472127..26db6784 100755
    --- a/src/test/lint/staticGate.sh
    +++ b/src/test/lint/staticGate.sh
    @@ -491,7 +491,7 @@ fi
     # its jobserver, and the query then prints directory lines into the value.
     REAL_CC="${DJGPP_CC:-$(env -u MAKEFLAGS -u MFLAGS -u MAKELEVEL make -s --no-print-directory -f "$ROOT/src/common.mk" --eval 'printCc: ; @echo $(CC)' printCc)}"
     # The copied tree has no toolchains/, so its make gets the real tree's.
    -read -ra TOOLCHAIN_ARGS < <(env -u MAKEFLAGS -u MFLAGS -u MAKELEVEL make -s --no-print-directory -f "$ROOT/src/common.mk" --eval 'printTc: ; @echo DJGPP_PREFIX=$(DJGPP_PREFIX) CWSDSTUB=$(CWSDSTUB)' printTc)
    +read -ra TOOLCHAIN_ARGS < <(env -u MAKEFLAGS -u MFLAGS -u MAKELEVEL make -s --no-print-directory -f "$ROOT/src/common.mk" --eval 'printTc: ; @echo DJGPP_PREFIX=$(DJGPP_PREFIX) CWSDSTUB=$(CWSDSTUB) FREETYPE_DIR=$(FREETYPE_DIR) FONT_SOURCES=$(FONT_SOURCES)' printTc)
     
     # ------------------------------------------------------------
     # cppcheck
    @@ -525,6 +525,7 @@ if [ "$RUN_CPPCHECK" -eq 1 ]; then
             -Isrc/libs/kpunch/libtasks
             -Isrc/libs/kpunch/texthelp
             -Isrc/libs/kpunch/listhelp
    +        -Isrc/libs/kpunch/richtext
             -Isrc/libs/kpunch/dvxshell
             -Isrc/libs/kpunch/taskmgr
             -Isrc/libs/kpunch/serial
    @@ -634,7 +635,7 @@ WRAPEOF
         # -Orecurse groups each sub-make's output so the "Entering directory"
         # lines below stay associated with their compiles under -j.
         # LC_ALL=C keeps gcc quoting ASCII ('x' not UTF-8 curly quotes).
    -    (cd "$COPY" && LC_ALL=C make -k -j"$(nproc)" -Orecurse "${TOOLCHAIN_ARGS[@]}" CC="$WRAP" HOSTCC="$HOSTWRAP" libdvx libtasks loader texthelp listhelp tools widgets dvxshell taskmgr serial sql apps > "$WORK/analyzer.raw" 2>&1)
    +    (cd "$COPY" && LC_ALL=C make -k -j"$(nproc)" -Orecurse "${TOOLCHAIN_ARGS[@]}" CC="$WRAP" HOSTCC="$HOSTWRAP" libdvx libtasks loader texthelp listhelp richtext tools widgets dvxshell taskmgr serial sql apps > "$WORK/analyzer.raw" 2>&1)
         compiles="$(grep -c "^$WRAP .* -c \|^$HOSTWRAP " "$WORK/analyzer.raw" || true)"
         makeErrors="$(grep -c '^make.*\*\*\*' "$WORK/analyzer.raw" || true)"
         echo "staticGate: analyzer compiled $compiles file(s)"
    diff --git a/src/test/samples/golden/basdemo-debug.app b/src/test/samples/golden/basdemo-debug.app
    index 5cbf9836..9e4da60d 100644
    Binary files a/src/test/samples/golden/basdemo-debug.app and b/src/test/samples/golden/basdemo-debug.app differ
    diff --git a/src/test/samples/golden/basdemo-release.app b/src/test/samples/golden/basdemo-release.app
    index 278c41f1..5649fd53 100644
    Binary files a/src/test/samples/golden/basdemo-release.app and b/src/test/samples/golden/basdemo-release.app differ
    diff --git a/src/test/samples/golden/clock-debug.app b/src/test/samples/golden/clock-debug.app
    index cbe6675f..6713921b 100644
    Binary files a/src/test/samples/golden/clock-debug.app and b/src/test/samples/golden/clock-debug.app differ
    diff --git a/src/test/samples/golden/clock-release.app b/src/test/samples/golden/clock-release.app
    index ab461370..f9fd3d85 100644
    Binary files a/src/test/samples/golden/clock-release.app and b/src/test/samples/golden/clock-release.app differ
    diff --git a/src/test/samples/golden/fileMgr-debug.app b/src/test/samples/golden/fileMgr-debug.app
    index eb8915c0..feb71950 100644
    Binary files a/src/test/samples/golden/fileMgr-debug.app and b/src/test/samples/golden/fileMgr-debug.app differ
    diff --git a/src/test/samples/golden/fileMgr-release.app b/src/test/samples/golden/fileMgr-release.app
    index b54ab8a1..c7b1ed1a 100644
    Binary files a/src/test/samples/golden/fileMgr-release.app and b/src/test/samples/golden/fileMgr-release.app differ
    diff --git a/src/test/samples/golden/helpedit-debug.app b/src/test/samples/golden/helpedit-debug.app
    index ee0ae1f1..550f548e 100644
    Binary files a/src/test/samples/golden/helpedit-debug.app and b/src/test/samples/golden/helpedit-debug.app differ
    diff --git a/src/test/samples/golden/helpedit-release.app b/src/test/samples/golden/helpedit-release.app
    index 2464ce87..e26480f4 100644
    Binary files a/src/test/samples/golden/helpedit-release.app and b/src/test/samples/golden/helpedit-release.app differ
    diff --git a/src/test/samples/golden/iconed-debug.app b/src/test/samples/golden/iconed-debug.app
    index 9e2c3df5..d3e25278 100644
    Binary files a/src/test/samples/golden/iconed-debug.app and b/src/test/samples/golden/iconed-debug.app differ
    diff --git a/src/test/samples/golden/iconed-release.app b/src/test/samples/golden/iconed-release.app
    index 167d6ce1..74b621e6 100644
    Binary files a/src/test/samples/golden/iconed-release.app and b/src/test/samples/golden/iconed-release.app differ
    diff --git a/src/test/samples/golden/imgview-debug.app b/src/test/samples/golden/imgview-debug.app
    index ef3b1b3b..20e08f8a 100644
    Binary files a/src/test/samples/golden/imgview-debug.app and b/src/test/samples/golden/imgview-debug.app differ
    diff --git a/src/test/samples/golden/imgview-release.app b/src/test/samples/golden/imgview-release.app
    index bb690d12..0f7c6f8b 100644
    Binary files a/src/test/samples/golden/imgview-release.app and b/src/test/samples/golden/imgview-release.app differ
    diff --git a/src/test/samples/golden/inc-comm-debug.app b/src/test/samples/golden/inc-comm-debug.app
    index d354049f..1a4e58a1 100644
    Binary files a/src/test/samples/golden/inc-comm-debug.app and b/src/test/samples/golden/inc-comm-debug.app differ
    diff --git a/src/test/samples/golden/inc-comm-release.app b/src/test/samples/golden/inc-comm-release.app
    index 249b7ea7..e4526d38 100644
    Binary files a/src/test/samples/golden/inc-comm-release.app and b/src/test/samples/golden/inc-comm-release.app differ
    diff --git a/src/test/samples/golden/inc-commdlg-debug.app b/src/test/samples/golden/inc-commdlg-debug.app
    index 5628dcf9..e8ae2ee4 100644
    Binary files a/src/test/samples/golden/inc-commdlg-debug.app and b/src/test/samples/golden/inc-commdlg-debug.app differ
    diff --git a/src/test/samples/golden/inc-commdlg-release.app b/src/test/samples/golden/inc-commdlg-release.app
    index 41d518df..a6039f3b 100644
    Binary files a/src/test/samples/golden/inc-commdlg-release.app and b/src/test/samples/golden/inc-commdlg-release.app differ
    diff --git a/src/test/samples/golden/inc-compiler-debug.app b/src/test/samples/golden/inc-compiler-debug.app
    index ab78d3d4..4d80479a 100644
    Binary files a/src/test/samples/golden/inc-compiler-debug.app and b/src/test/samples/golden/inc-compiler-debug.app differ
    diff --git a/src/test/samples/golden/inc-compiler-release.app b/src/test/samples/golden/inc-compiler-release.app
    index 7ecdfc99..657ea75f 100644
    Binary files a/src/test/samples/golden/inc-compiler-release.app and b/src/test/samples/golden/inc-compiler-release.app differ
    diff --git a/src/test/samples/golden/inc-conn-debug.app b/src/test/samples/golden/inc-conn-debug.app
    index 07fd40fe..713cad48 100644
    Binary files a/src/test/samples/golden/inc-conn-debug.app and b/src/test/samples/golden/inc-conn-debug.app differ
    diff --git a/src/test/samples/golden/inc-conn-release.app b/src/test/samples/golden/inc-conn-release.app
    index 561e7bc3..74e0cbe8 100644
    Binary files a/src/test/samples/golden/inc-conn-release.app and b/src/test/samples/golden/inc-conn-release.app differ
    diff --git a/src/test/samples/golden/inc-debug-debug.app b/src/test/samples/golden/inc-debug-debug.app
    index d729089b..a2df4ee9 100644
    Binary files a/src/test/samples/golden/inc-debug-debug.app and b/src/test/samples/golden/inc-debug-debug.app differ
    diff --git a/src/test/samples/golden/inc-debug-release.app b/src/test/samples/golden/inc-debug-release.app
    index 85e849f4..cb781acf 100644
    Binary files a/src/test/samples/golden/inc-debug-release.app and b/src/test/samples/golden/inc-debug-release.app differ
    diff --git a/src/test/samples/golden/inc-designer-debug.app b/src/test/samples/golden/inc-designer-debug.app
    index 3f8fe80a..f8ea2580 100644
    Binary files a/src/test/samples/golden/inc-designer-debug.app and b/src/test/samples/golden/inc-designer-debug.app differ
    diff --git a/src/test/samples/golden/inc-designer-release.app b/src/test/samples/golden/inc-designer-release.app
    index c75ba416..8c0b2e8c 100644
    Binary files a/src/test/samples/golden/inc-designer-release.app and b/src/test/samples/golden/inc-designer-release.app differ
    diff --git a/src/test/samples/golden/inc-fileops-debug.app b/src/test/samples/golden/inc-fileops-debug.app
    index 254167c0..e9a8c712 100644
    Binary files a/src/test/samples/golden/inc-fileops-debug.app and b/src/test/samples/golden/inc-fileops-debug.app differ
    diff --git a/src/test/samples/golden/inc-fileops-release.app b/src/test/samples/golden/inc-fileops-release.app
    index 05089d96..67bc7a3c 100644
    Binary files a/src/test/samples/golden/inc-fileops-release.app and b/src/test/samples/golden/inc-fileops-release.app differ
    diff --git a/src/test/samples/golden/inc-font-debug.app b/src/test/samples/golden/inc-font-debug.app
    new file mode 100644
    index 00000000..ba18974d
    Binary files /dev/null and b/src/test/samples/golden/inc-font-debug.app differ
    diff --git a/src/test/samples/golden/inc-font-release.app b/src/test/samples/golden/inc-font-release.app
    new file mode 100644
    index 00000000..7bafd949
    Binary files /dev/null and b/src/test/samples/golden/inc-font-release.app differ
    diff --git a/src/test/samples/golden/inc-game-debug.app b/src/test/samples/golden/inc-game-debug.app
    index 075d0fb6..f7ff9311 100644
    Binary files a/src/test/samples/golden/inc-game-debug.app and b/src/test/samples/golden/inc-game-debug.app differ
    diff --git a/src/test/samples/golden/inc-game-release.app b/src/test/samples/golden/inc-game-release.app
    index b9c4bbbc..fe597aad 100644
    Binary files a/src/test/samples/golden/inc-game-release.app and b/src/test/samples/golden/inc-game-release.app differ
    diff --git a/src/test/samples/golden/inc-help-debug.app b/src/test/samples/golden/inc-help-debug.app
    index 2f597f9a..df90dccb 100644
    Binary files a/src/test/samples/golden/inc-help-debug.app and b/src/test/samples/golden/inc-help-debug.app differ
    diff --git a/src/test/samples/golden/inc-help-release.app b/src/test/samples/golden/inc-help-release.app
    index 98c20f20..322dc31d 100644
    Binary files a/src/test/samples/golden/inc-help-release.app and b/src/test/samples/golden/inc-help-release.app differ
    diff --git a/src/test/samples/golden/inc-hlpfile-debug.app b/src/test/samples/golden/inc-hlpfile-debug.app
    index 435dc8d4..a949e3a5 100644
    Binary files a/src/test/samples/golden/inc-hlpfile-debug.app and b/src/test/samples/golden/inc-hlpfile-debug.app differ
    diff --git a/src/test/samples/golden/inc-hlpfile-release.app b/src/test/samples/golden/inc-hlpfile-release.app
    index dc232f5b..98e34ad1 100644
    Binary files a/src/test/samples/golden/inc-hlpfile-release.app and b/src/test/samples/golden/inc-hlpfile-release.app differ
    diff --git a/src/test/samples/golden/inc-joystick-debug.app b/src/test/samples/golden/inc-joystick-debug.app
    index 14b09067..9cbcb6ce 100644
    Binary files a/src/test/samples/golden/inc-joystick-debug.app and b/src/test/samples/golden/inc-joystick-debug.app differ
    diff --git a/src/test/samples/golden/inc-joystick-release.app b/src/test/samples/golden/inc-joystick-release.app
    index 584ace05..e6dd564e 100644
    Binary files a/src/test/samples/golden/inc-joystick-release.app and b/src/test/samples/golden/inc-joystick-release.app differ
    diff --git a/src/test/samples/golden/inc-layout-debug.app b/src/test/samples/golden/inc-layout-debug.app
    index 9d8a98cb..5eb97dc6 100644
    Binary files a/src/test/samples/golden/inc-layout-debug.app and b/src/test/samples/golden/inc-layout-debug.app differ
    diff --git a/src/test/samples/golden/inc-layout-release.app b/src/test/samples/golden/inc-layout-release.app
    index a7c16198..85f1665a 100644
    Binary files a/src/test/samples/golden/inc-layout-release.app and b/src/test/samples/golden/inc-layout-release.app differ
    diff --git a/src/test/samples/golden/inc-listhelp-debug.app b/src/test/samples/golden/inc-listhelp-debug.app
    index add904ef..a51c7ff5 100644
    Binary files a/src/test/samples/golden/inc-listhelp-debug.app and b/src/test/samples/golden/inc-listhelp-debug.app differ
    diff --git a/src/test/samples/golden/inc-listhelp-release.app b/src/test/samples/golden/inc-listhelp-release.app
    index db49ebf4..4568c7d1 100644
    Binary files a/src/test/samples/golden/inc-listhelp-release.app and b/src/test/samples/golden/inc-listhelp-release.app differ
    diff --git a/src/test/samples/golden/inc-music-debug.app b/src/test/samples/golden/inc-music-debug.app
    index 0689b06f..398a4722 100644
    Binary files a/src/test/samples/golden/inc-music-debug.app and b/src/test/samples/golden/inc-music-debug.app differ
    diff --git a/src/test/samples/golden/inc-music-release.app b/src/test/samples/golden/inc-music-release.app
    index 40b87a87..e77a0f81 100644
    Binary files a/src/test/samples/golden/inc-music-release.app and b/src/test/samples/golden/inc-music-release.app differ
    diff --git a/src/test/samples/golden/inc-net-debug.app b/src/test/samples/golden/inc-net-debug.app
    index 2cab6397..d15a2ff0 100644
    Binary files a/src/test/samples/golden/inc-net-debug.app and b/src/test/samples/golden/inc-net-debug.app differ
    diff --git a/src/test/samples/golden/inc-net-release.app b/src/test/samples/golden/inc-net-release.app
    index 0fe18944..1ba288b8 100644
    Binary files a/src/test/samples/golden/inc-net-release.app and b/src/test/samples/golden/inc-net-release.app differ
    diff --git a/src/test/samples/golden/inc-prefs-debug.app b/src/test/samples/golden/inc-prefs-debug.app
    index f45f19e9..c078cbcf 100644
    Binary files a/src/test/samples/golden/inc-prefs-debug.app and b/src/test/samples/golden/inc-prefs-debug.app differ
    diff --git a/src/test/samples/golden/inc-prefs-release.app b/src/test/samples/golden/inc-prefs-release.app
    index 768db093..6878bf1c 100644
    Binary files a/src/test/samples/golden/inc-prefs-release.app and b/src/test/samples/golden/inc-prefs-release.app differ
    diff --git a/src/test/samples/golden/inc-print-debug.app b/src/test/samples/golden/inc-print-debug.app
    index 80ae18b2..648ddf3a 100644
    Binary files a/src/test/samples/golden/inc-print-debug.app and b/src/test/samples/golden/inc-print-debug.app differ
    diff --git a/src/test/samples/golden/inc-print-release.app b/src/test/samples/golden/inc-print-release.app
    index cd0d0529..440a499a 100644
    Binary files a/src/test/samples/golden/inc-print-release.app and b/src/test/samples/golden/inc-print-release.app differ
    diff --git a/src/test/samples/golden/inc-resource-debug.app b/src/test/samples/golden/inc-resource-debug.app
    index 0304d96d..c2f5cbf0 100644
    Binary files a/src/test/samples/golden/inc-resource-debug.app and b/src/test/samples/golden/inc-resource-debug.app differ
    diff --git a/src/test/samples/golden/inc-resource-release.app b/src/test/samples/golden/inc-resource-release.app
    index efb57227..dcf626bb 100644
    Binary files a/src/test/samples/golden/inc-resource-release.app and b/src/test/samples/golden/inc-resource-release.app differ
    diff --git a/src/test/samples/golden/inc-richtext-debug.app b/src/test/samples/golden/inc-richtext-debug.app
    new file mode 100644
    index 00000000..53686d22
    Binary files /dev/null and b/src/test/samples/golden/inc-richtext-debug.app differ
    diff --git a/src/test/samples/golden/inc-richtext-release.app b/src/test/samples/golden/inc-richtext-release.app
    new file mode 100644
    index 00000000..0b56e59e
    Binary files /dev/null and b/src/test/samples/golden/inc-richtext-release.app differ
    diff --git a/src/test/samples/golden/inc-security-debug.app b/src/test/samples/golden/inc-security-debug.app
    index e096c143..0f7d3916 100644
    Binary files a/src/test/samples/golden/inc-security-debug.app and b/src/test/samples/golden/inc-security-debug.app differ
    diff --git a/src/test/samples/golden/inc-security-release.app b/src/test/samples/golden/inc-security-release.app
    index 4728ed85..de486c88 100644
    Binary files a/src/test/samples/golden/inc-security-release.app and b/src/test/samples/golden/inc-security-release.app differ
    diff --git a/src/test/samples/golden/inc-shell-debug.app b/src/test/samples/golden/inc-shell-debug.app
    index 838ef101..7e03bb21 100644
    Binary files a/src/test/samples/golden/inc-shell-debug.app and b/src/test/samples/golden/inc-shell-debug.app differ
    diff --git a/src/test/samples/golden/inc-shell-release.app b/src/test/samples/golden/inc-shell-release.app
    index 6a7eb18c..f1093f79 100644
    Binary files a/src/test/samples/golden/inc-shell-release.app and b/src/test/samples/golden/inc-shell-release.app differ
    diff --git a/src/test/samples/golden/inc-sound-debug.app b/src/test/samples/golden/inc-sound-debug.app
    index 74cc3ccb..244d9550 100644
    Binary files a/src/test/samples/golden/inc-sound-debug.app and b/src/test/samples/golden/inc-sound-debug.app differ
    diff --git a/src/test/samples/golden/inc-sound-release.app b/src/test/samples/golden/inc-sound-release.app
    index 9004cd61..dc4a1bb8 100644
    Binary files a/src/test/samples/golden/inc-sound-release.app and b/src/test/samples/golden/inc-sound-release.app differ
    diff --git a/src/test/samples/golden/inc-sql-debug.app b/src/test/samples/golden/inc-sql-debug.app
    index 321a7863..5f7b4f6b 100644
    Binary files a/src/test/samples/golden/inc-sql-debug.app and b/src/test/samples/golden/inc-sql-debug.app differ
    diff --git a/src/test/samples/golden/inc-sql-release.app b/src/test/samples/golden/inc-sql-release.app
    index 04bfdc30..eb65f29f 100644
    Binary files a/src/test/samples/golden/inc-sql-release.app and b/src/test/samples/golden/inc-sql-release.app differ
    diff --git a/src/test/samples/golden/inc-system-debug.app b/src/test/samples/golden/inc-system-debug.app
    index 63e4b0b7..dd3a35c6 100644
    Binary files a/src/test/samples/golden/inc-system-debug.app and b/src/test/samples/golden/inc-system-debug.app differ
    diff --git a/src/test/samples/golden/inc-system-release.app b/src/test/samples/golden/inc-system-release.app
    index 8e2f43e0..d9165c48 100644
    Binary files a/src/test/samples/golden/inc-system-release.app and b/src/test/samples/golden/inc-system-release.app differ
    diff --git a/src/test/samples/golden/inc-xfer-debug.app b/src/test/samples/golden/inc-xfer-debug.app
    index d1a535f2..d1f1dd77 100644
    Binary files a/src/test/samples/golden/inc-xfer-debug.app and b/src/test/samples/golden/inc-xfer-debug.app differ
    diff --git a/src/test/samples/golden/inc-xfer-release.app b/src/test/samples/golden/inc-xfer-release.app
    index f1dc2ba8..f418b020 100644
    Binary files a/src/test/samples/golden/inc-xfer-release.app and b/src/test/samples/golden/inc-xfer-release.app differ
    diff --git a/src/test/samples/golden/notepad-debug.app b/src/test/samples/golden/notepad-debug.app
    index d91eb9fb..a1d52fb4 100644
    Binary files a/src/test/samples/golden/notepad-debug.app and b/src/test/samples/golden/notepad-debug.app differ
    diff --git a/src/test/samples/golden/notepad-release.app b/src/test/samples/golden/notepad-release.app
    index f6ad77d8..4add451e 100644
    Binary files a/src/test/samples/golden/notepad-release.app and b/src/test/samples/golden/notepad-release.app differ
    diff --git a/src/test/samples/golden/resedit-debug.app b/src/test/samples/golden/resedit-debug.app
    index d363b754..612b921d 100644
    Binary files a/src/test/samples/golden/resedit-debug.app and b/src/test/samples/golden/resedit-debug.app differ
    diff --git a/src/test/samples/golden/resedit-release.app b/src/test/samples/golden/resedit-release.app
    index 439024d3..35995500 100644
    Binary files a/src/test/samples/golden/resedit-release.app and b/src/test/samples/golden/resedit-release.app differ
    diff --git a/src/test/samples/golden/terminal-debug.app b/src/test/samples/golden/terminal-debug.app
    index 5e5423e0..3e1fa9fe 100644
    Binary files a/src/test/samples/golden/terminal-debug.app and b/src/test/samples/golden/terminal-debug.app differ
    diff --git a/src/test/samples/golden/terminal-release.app b/src/test/samples/golden/terminal-release.app
    index a3c2fce8..503f9423 100644
    Binary files a/src/test/samples/golden/terminal-release.app and b/src/test/samples/golden/terminal-release.app differ
    diff --git a/src/test/samples/golden/welcome-debug.app b/src/test/samples/golden/welcome-debug.app
    index d35b4c9b..039d257f 100644
    Binary files a/src/test/samples/golden/welcome-debug.app and b/src/test/samples/golden/welcome-debug.app differ
    diff --git a/src/test/samples/golden/welcome-release.app b/src/test/samples/golden/welcome-release.app
    index ce24963d..d9b347f7 100644
    Binary files a/src/test/samples/golden/welcome-release.app and b/src/test/samples/golden/welcome-release.app differ
    diff --git a/src/test/samples/golden/widshow-debug.app b/src/test/samples/golden/widshow-debug.app
    index 32cf75b2..b6a24cb1 100644
    Binary files a/src/test/samples/golden/widshow-debug.app and b/src/test/samples/golden/widshow-debug.app differ
    diff --git a/src/test/samples/golden/widshow-release.app b/src/test/samples/golden/widshow-release.app
    index 6e2ce7a4..7f1b5569 100644
    Binary files a/src/test/samples/golden/widshow-release.app and b/src/test/samples/golden/widshow-release.app differ
    diff --git a/src/test/sdk/sdkTest.sh b/src/test/sdk/sdkTest.sh
    index fe4f63e6..5ee3b330 100755
    --- a/src/test/sdk/sdkTest.sh
    +++ b/src/test/sdk/sdkTest.sh
    @@ -303,7 +303,7 @@ if [ ! -x "$SDK/newProject.sh" ]; then
         exit 1
     fi
     
    -for f in README.TXT sdk.mk bin/dvxres bin/dvxhlpc bin/bascomp bin/dxeGen.sh; do
    +for f in README.TXT sdk.mk bin/dvxres bin/dvxhlpc bin/bascomp bin/mkfont bin/dxeGen.sh; do
         check "SDK ships $f" test -e "$SDK/$f"
     done
     
    diff --git a/src/test/setup/setupTest.c b/src/test/setup/setupTest.c
    index 84512db9..cd3ee02d 100644
    --- a/src/test/setup/setupTest.c
    +++ b/src/test/setup/setupTest.c
    @@ -86,6 +86,9 @@
     #define SIM_OTHER_SET    (-1)   // a part of another set
     #define SIM_GARBAGE      (-2)   // a file that is no part at all
     #define SIM_CANCEL       0      // the user gives up
    +#define TIER_ALL         (PKG_TIERS - 1)   // the whole package
    +#define TIER_BASE_FILES  2      // sTierFonts: the base set's files, then the others'
    +#define TIER_MORE_FILES  2
     
     // The package tree every case builds from
     static const char *const sTreeFiles[TREE_FILES][2] = {
    @@ -97,6 +100,15 @@ static const char *const sTreeFiles[TREE_FILES][2] = {
         { "libs/big.lib",       NULL }            // BIG_FILE_BYTES of varied text
     };
     
    +// The fonts caseTiers adds to the tree: the base set's (small), then the
    +// others' (big, varied text: they fill disks of their own)
    +static const char *const sTierFonts[TIER_BASE_FILES + TIER_MORE_FILES] = {
    +    "FONTS/BASE1.DFN",
    +    "FONTS/LICENSES/BASE.TXT",
    +    "FONTS/MORE1.DFN",
    +    "FONTS/MORE2.DFN"
    +};
    +
     // A floppy drive for the disk-swap cases: askDisk "inserts" the next
     // scripted disk into drive (a folder) and logs what it was asked.
     typedef struct {
    @@ -125,6 +137,7 @@ static void     caseInstallAppended(void);
     static void     caseReadOnly(void);
     static void     caseSpaceAndIni(void);
     static void     caseSplitSwap(void);
    +static void     caseTiers(void);
     static void     caseUpgradeUninstall(void);
     static void     caseWelcomeApp(void);
     static void     clearDrive(const char *drive);
    @@ -134,7 +147,7 @@ static void     insertPart(const char *fromDir, int32_t disk, const char *drive)
     static bool     keepSetting(void *user, const char *relName);
     static void     makePackage(const char *name, char *zip, char *exe);
     static int32_t  readVideoWidth(const char *iniPath);
    -static PkgStepE runJob(const PkgSourceT *src, const char *dest, PkgAskDiskFnT ask, PkgKeepFnT keep, void *user, char *msg, char ***files);
    +static PkgStepE runJob(const PkgSourceT *src, const char *dest, int32_t tier, PkgAskDiskFnT ask, PkgKeepFnT keep, void *user, char *msg, char ***files);
     static bool     simAsk(void *user, int32_t disk, PkgDiskProblemE problem);
     static void     tp(char *out, const char *leaf);
     static bool     treeMatches(const char *dest);
    @@ -245,7 +258,7 @@ static void caseCancel(void) {
         tp(dest, "cancelDest");
         TEST_EQ(platformMkdirRecursive(dest), 0);
     
    -    PkgJobT *job  = pkgJobCreate(&src, dest, NULL, NULL, NULL);
    +    PkgJobT *job  = pkgJobCreate(&src, dest, TIER_ALL, NULL, NULL, NULL);
         PkgStepE step = PkgStepMoreE;
     
         TEST_TRUE(job != NULL);
    @@ -344,7 +357,7 @@ static void caseCrc(void) {
         TEST_EQ(src.found, PkgFoundAppendedE);
         tp(dest, "crcDest");
         TEST_EQ(platformMkdirRecursive(dest), 0);
    -    TEST_EQ(runJob(&src, dest, NULL, NULL, NULL, msg, NULL), PkgStepErrorE);
    +    TEST_EQ(runJob(&src, dest, TIER_ALL, NULL, NULL, NULL, msg, NULL), PkgStepErrorE);
         TEST_TRUE(strstr(msg, "damaged") != NULL);
         TEST_TRUE(strstr(msg, name) != NULL);
         TEST_TRUE(pkgLocalPath(dest, name, path, sizeof(path)));
    @@ -368,7 +381,7 @@ static void caseEmptyLists(void) {
         snprintf(dir, sizeof(dir), "%s/emptyPkg", tTempDir());
         TEST_EQ(platformMkdirRecursive(dir), 0);
         snprintf(zip, sizeof(zip), "%s/empty.zip", tTempDir());
    -    TEST_TRUE(pkgBuildZip(dir, zip, err, sizeof(err)));
    +    TEST_TRUE(pkgBuildZip(dir, zip, NULL, 0, err, sizeof(err)));
         TEST_TRUE(exists(zip));
     
         snprintf(dir, sizeof(dir), "%s/flatDest", tTempDir());
    @@ -425,12 +438,27 @@ static void caseFormat(void) {
         h.partOffset    = 1000;
         h.partLength    = 500;
         h.zipSize       = 2000;
    -    h.fileCount     = 42;
    -    h.unpackedBytes = 0x123456789ull;
    +
    +    for (int32_t t = 0; t < PKG_TIERS; t++) {
    +        h.tiers[t].end       = h.zipSize - (uint32_t)(TIER_ALL - t) * 100;
    +        h.tiers[t].fileCount = (uint32_t)(40 + t);
    +        h.tiers[t].bytes     = 0x123456789ull + (uint64_t)t;
    +    }
    +
         pkgHeaderEncode(&h, buf);
         TEST_TRUE(pkgHeaderDecode(buf, &back));
         TEST_TRUE(memcmp(&h, &back, sizeof(h)) == 0);
     
    +    // A tier ending before the one before it, or past the ZIP
    +    h.tiers[1].end = h.tiers[0].end - 1;
    +    pkgHeaderEncode(&h, buf);
    +    TEST_TRUE(!pkgHeaderDecode(buf, &back));
    +    h.tiers[1].end       = h.tiers[0].end;
    +    h.tiers[TIER_ALL].end = h.zipSize + 1;
    +    pkgHeaderEncode(&h, buf);
    +    TEST_TRUE(!pkgHeaderDecode(buf, &back));
    +    h.tiers[TIER_ALL].end = h.zipSize;
    +
         buf[20] ^= 1;
         TEST_TRUE(!pkgHeaderDecode(buf, &back));
     
    @@ -492,7 +520,7 @@ static void caseInstallAppended(void) {
         pkgFind(exe, &src);
         TEST_EQ(src.found, PkgFoundAppendedE);
         TEST_EQ(src.info.diskCount, 1);
    -    TEST_EQ(src.info.fileCount, TREE_FILES);
    +    TEST_EQ(src.info.tiers[TIER_ALL].fileCount, TREE_FILES);
     
         // The program in front of the package is untouched.
         int32_t len  = 0;
    @@ -503,7 +531,7 @@ static void caseInstallAppended(void) {
     
         tp(dest, "cdDest");
         TEST_EQ(platformMkdirRecursive(dest), 0);
    -    TEST_EQ(runJob(&src, dest, NULL, NULL, NULL, msg, &files), PkgStepDoneE);
    +    TEST_EQ(runJob(&src, dest, TIER_ALL, NULL, NULL, NULL, msg, &files), PkgStepDoneE);
         TEST_TRUE(treeMatches(dest));
         snprintf(path, sizeof(path), "%s/empty", dest);
         TEST_TRUE(exists(path));
    @@ -614,9 +642,12 @@ static void caseSpaceAndIni(void) {
         char       path[PATH_MAX_T];
     
         memset(&info, 0, sizeof(info));
    -    info.unpackedBytes = 1000;
    -    info.fileCount     = 3;
    -    TEST_EQ(setupSpaceNeeded(&info), (uint64_t)(1000 + 3 * SETUP_FILE_SLACK));
    +    info.tiers[SetupFontsNoneE].bytes     = 400;
    +    info.tiers[SetupFontsNoneE].fileCount = 1;
    +    info.tiers[SetupFontsAllE].bytes      = 1000;
    +    info.tiers[SetupFontsAllE].fileCount  = 3;
    +    TEST_EQ(setupSpaceNeeded(&info, SetupFontsNoneE), (uint64_t)(400 + SETUP_FILE_SLACK));
    +    TEST_EQ(setupSpaceNeeded(&info, SetupFontsAllE), (uint64_t)(1000 + 3 * SETUP_FILE_SLACK));
     
         tp(dir, "ini");
         snprintf(path, sizeof(path), "%s/%s", dir, DVX_CONFIG_DIR);
    @@ -688,7 +719,7 @@ static void caseSplitSwap(void) {
         TEST_EQ(src.info.diskCount, disks);
         tp(dest, "splitAll");
         TEST_EQ(platformMkdirRecursive(dest), 0);
    -    TEST_EQ(runJob(&src, dest, NULL, NULL, NULL, msg, NULL), PkgStepDoneE);
    +    TEST_EQ(runJob(&src, dest, TIER_ALL, NULL, NULL, NULL, msg, NULL), PkgStepDoneE);
         TEST_TRUE(treeMatches(dest));
     
         // One drive: Disk 1 in it; then for Disk 2 the user inserts another
    @@ -713,7 +744,7 @@ static void caseSplitSwap(void) {
         TEST_EQ(src.found, PkgFoundPartsE);
         tp(dest, "splitSwap");
         TEST_EQ(platformMkdirRecursive(dest), 0);
    -    TEST_EQ(runJob(&src, dest, simAsk, NULL, &sim, msg, NULL), PkgStepDoneE);
    +    TEST_EQ(runJob(&src, dest, TIER_ALL, simAsk, NULL, &sim, msg, NULL), PkgStepDoneE);
         TEST_TRUE(treeMatches(dest));
         TEST_TRUE(strncmp(sim.log, "2:missing 2:otherset 2:damaged 2:wrong 3:missing ", strlen("2:missing 2:otherset 2:damaged 2:wrong 3:missing ")) == 0);
     
    @@ -727,11 +758,128 @@ static void caseSplitSwap(void) {
         tp(dest, "splitCancel");
         TEST_EQ(platformMkdirRecursive(dest), 0);
         TEST_EQ(pkgPartInDir(dest), 0);
    -    TEST_EQ(runJob(&src, dest, simAsk, NULL, &sim, msg, NULL), PkgStepCancelledE);
    +    TEST_EQ(runJob(&src, dest, TIER_ALL, simAsk, NULL, &sim, msg, NULL), PkgStepCancelledE);
         TEST_STR_EQ(sim.log, "2:missing ");
     }
     
     
    +// Tiers: the fonts come after the program, the base set first, their
    +// files marked with their tiers; an unpack stops at the tier chosen and
    +// asks for no disk after it; an upgrade to fewer fonts removes the
    +// others; setupFontsInstalled tells what a folder has.
    +static void caseTiers(void) {
    +    tCase("tiers: fonts after the program, an unpack stops at its tier, fewer fonts on an upgrade");
    +
    +    const char *const  base[]  = { "FONTS/", "FONTS/LICENSES/", "FONTS/BASE1.DFN", "fonts/licenses/base.txt" };
    +    const char *const  more[]  = { "fonts" PKG_TIER_WILD };
    +    const PkgTierRuleT rules[] = { { base, DVX_ARRAY_LEN(base) }, { more, DVX_ARRAY_LEN(more) } };
    +    char               tree[PATH_MAX_T];
    +    char               zip[PATH_MAX_T];
    +    char               exe[PATH_MAX_T];
    +    char               dest[PATH_MAX_T];
    +    char               msg[PKG_MSG_MAX];
    +    char               err[PKG_BUILD_ERR_MAX];
    +    char               path[PATH_MAX_T];
    +    char               text[BIG_FILE_BYTES + 1];
    +    PkgHeaderT         info;
    +    PkgSourceT         src;
    +    DiskSimT           sim;
    +
    +    tp(tree, "tierTree");
    +    buildTree(tree);
    +    treeText(TREE_FILES - 1, text, sizeof(text));
    +
    +    for (int32_t i = 0; i < DVX_ARRAY_LEN(sTierFonts); i++) {
    +        TEST_TRUE(pkgLocalPath(tree, sTierFonts[i], path, sizeof(path)));
    +        *strrchr(path, DVX_PATH_SEP[0]) = '\0';
    +        TEST_EQ(platformMkdirRecursive(path), 0);
    +        TEST_TRUE(pkgLocalPath(tree, sTierFonts[i], path, sizeof(path)));
    +        writeText(path, (i < TIER_BASE_FILES) ? sTierFonts[i] : text);
    +    }
    +
    +    // Each tier's files and end, the base set's (rules in any case) first
    +    snprintf(zip, sizeof(zip), "%s/tier.zip", tTempDir());
    +    TEST_TRUE(pkgBuildZip(tree, zip, rules, DVX_ARRAY_LEN(rules), err, sizeof(err)));
    +    TEST_TRUE(pkgBuildInfo(zip, &info, err, sizeof(err)));
    +    TEST_EQ(info.tiers[SetupFontsNoneE].fileCount, TREE_FILES);
    +    TEST_EQ(info.tiers[SetupFontsBaseE].fileCount, TREE_FILES + TIER_BASE_FILES);
    +    TEST_EQ(info.tiers[SetupFontsAllE].fileCount, TREE_FILES + TIER_BASE_FILES + TIER_MORE_FILES);
    +    TEST_TRUE(info.tiers[SetupFontsNoneE].end < info.tiers[SetupFontsBaseE].end);
    +    TEST_TRUE(info.tiers[SetupFontsBaseE].end < info.tiers[SetupFontsAllE].end);
    +    TEST_TRUE(info.tiers[SetupFontsAllE].end < info.zipSize);
    +    TEST_TRUE(info.tiers[SetupFontsBaseE].bytes - info.tiers[SetupFontsNoneE].bytes < BIG_FILE_BYTES);
    +
    +    // Floppies: the base set ends on an earlier disk than the last
    +    memset(&sim, 0, sizeof(sim));
    +    tp(sim.parts, "tierParts");
    +    TEST_EQ(platformMkdirRecursive(sim.parts), 0);
    +
    +    int32_t disks    = pkgBuildSplit(zip, sim.parts, SPLIT_ROOM, SPLIT_ROOM, err, sizeof(err));
    +    int32_t baseDisk = (int32_t)((info.tiers[SetupFontsBaseE].end - 1) / (SPLIT_ROOM - PKG_HEADER_SIZE)) + 1;
    +
    +    TEST_TRUE(baseDisk < disks);
    +
    +    // The base set from the drive: the disks up to its end are asked
    +    // for, then no more (another prompt would cancel the job)
    +    tp(sim.drive, "tierDrive");
    +    TEST_EQ(platformMkdirRecursive(sim.drive), 0);
    +    snprintf(exe, sizeof(exe), "%s/SETUP.EXE", sim.drive);
    +    writeText(exe, FAKE_EXE);
    +    insertPart(sim.parts, 1, sim.drive);
    +
    +    for (int32_t d = 2; d <= baseDisk; d++) {
    +        sim.actions[sim.actionCount++] = d;
    +    }
    +
    +    pkgFind(exe, &src);
    +    tp(dest, "tierBase");
    +    TEST_EQ(platformMkdirRecursive(dest), 0);
    +    TEST_EQ(runJob(&src, dest, SetupFontsBaseE, simAsk, NULL, &sim, msg, NULL), PkgStepDoneE);
    +    TEST_EQ(sim.next, baseDisk - 1);
    +    TEST_TRUE(treeMatches(dest));
    +    snprintf(path, sizeof(path), "%s/%s", dest, sTierFonts[0]);
    +    TEST_TRUE(exists(path));
    +    snprintf(path, sizeof(path), "%s/%s", dest, sTierFonts[TIER_BASE_FILES]);
    +    TEST_TRUE(!exists(path));
    +
    +    // All, then upgrades to the base set and to none: each removes the
    +    // fonts it leaves out (and their emptied folders) and says what the
    +    // folder has; a folder without DVX gets all of them
    +    snprintf(exe, sizeof(exe), "%s/SETUP.EXE", sim.parts);
    +    writeText(exe, FAKE_EXE);
    +    pkgFind(exe, &src);
    +    tp(dest, "tierUpgrade");
    +    TEST_EQ(platformMkdirRecursive(dest), 0);
    +    TEST_EQ(setupFontsInstalled(dest, &src.info), SetupFontsAllE);
    +
    +    for (int32_t fonts = SetupFontsAllE; fonts >= SetupFontsNoneE; fonts--) {
    +        char **files = NULL;
    +
    +        TEST_EQ(runJob(&src, dest, fonts, NULL, NULL, NULL, msg, &files), PkgStepDoneE);
    +        setupRemoveDropped(dest, SETUP_FONTS_FOLDER, files, (int32_t)arrlen(files));
    +        TEST_TRUE(setupListWrite(dest, files, (int32_t)arrlen(files)));
    +        TEST_EQ(setupFontsInstalled(dest, &src.info), fonts);
    +
    +        for (int32_t i = 0; i < DVX_ARRAY_LEN(sTierFonts); i++) {
    +            int32_t tier = (i < TIER_BASE_FILES) ? SetupFontsBaseE : SetupFontsAllE;
    +
    +            snprintf(path, sizeof(path), "%s/%s", dest, sTierFonts[i]);
    +            TEST_EQ(exists(path), tier <= fonts);
    +        }
    +
    +        for (int32_t i = 0; i < (int32_t)arrlen(files); i++) {
    +            free(files[i]);
    +        }
    +
    +        arrfree(files);
    +    }
    +
    +    snprintf(path, sizeof(path), "%s/FONTS", dest);
    +    TEST_TRUE(!exists(path));
    +    TEST_TRUE(treeMatches(dest));
    +}
    +
    +
     // An upgrade keeps CONFIG and replaces the rest; the list covers both
     // installs; Uninstall removes what was installed (settings only when
     // asked) and leaves the user's own files.
    @@ -751,7 +899,7 @@ static void caseUpgradeUninstall(void) {
         pkgFind(exe, &src);
         tp(dest, "upDest");
         TEST_EQ(platformMkdirRecursive(dest), 0);
    -    TEST_EQ(runJob(&src, dest, NULL, NULL, NULL, msg, &files), PkgStepDoneE);
    +    TEST_EQ(runJob(&src, dest, TIER_ALL, NULL, NULL, NULL, msg, &files), PkgStepDoneE);
         TEST_TRUE(setupIsInstalled(dest));
     
         // A file only an older version had, listed; the user's settings; a
    @@ -775,7 +923,7 @@ static void caseUpgradeUninstall(void) {
         arrfree(files);
         files = NULL;
     
    -    TEST_EQ(runJob(&src, dest, NULL, keepSetting, NULL, msg, &files), PkgStepDoneE);
    +    TEST_EQ(runJob(&src, dest, TIER_ALL, NULL, keepSetting, NULL, msg, &files), PkgStepDoneE);
         snprintf(path, sizeof(path), "%s/%s", dest, DVX_INI_PATH);
         TEST_TRUE(fileIs(path, "[user]\r\nmine = yes\r\n"));
         snprintf(path, sizeof(path), "%s/%s", dest, "dvx.exe");
    @@ -814,7 +962,7 @@ static void caseUpgradeUninstall(void) {
     
         // Install again, remove everything with the settings: only the user's
         // file (and so the folder) is left.
    -    TEST_EQ(runJob(&src, dest, NULL, NULL, NULL, msg, NULL), PkgStepDoneE);
    +    TEST_EQ(runJob(&src, dest, TIER_ALL, NULL, NULL, NULL, msg, NULL), PkgStepDoneE);
         TEST_TRUE(setupListWrite(dest, files, (int32_t)arrlen(files)));
         TEST_TRUE(setupUninstall(dest, true, err, sizeof(err)));
     
    @@ -827,7 +975,7 @@ static void caseUpgradeUninstall(void) {
         // Without the user's file the folder goes as well.
         snprintf(path, sizeof(path), "%s/%s", dest, "mine.txt");
         remove(path);
    -    TEST_EQ(runJob(&src, dest, NULL, NULL, NULL, msg, NULL), PkgStepDoneE);
    +    TEST_EQ(runJob(&src, dest, TIER_ALL, NULL, NULL, NULL, msg, NULL), PkgStepDoneE);
         TEST_TRUE(setupListWrite(dest, files, (int32_t)arrlen(files)));
         TEST_TRUE(setupUninstall(dest, true, err, sizeof(err)));
         TEST_TRUE(!exists(dest));
    @@ -995,7 +1143,7 @@ static void makePackage(const char *name, char *zip, char *exe) {
         snprintf(prog, sizeof(prog), "%s/%s/PROG.EXE", tTempDir(), name);
         snprintf(exe, PATH_MAX_T, "%s/%s/SETUP.EXE", tTempDir(), name);
         writeText(prog, FAKE_EXE);
    -    TEST_TRUE(pkgBuildZip(tree, zip, err, sizeof(err)));
    +    TEST_TRUE(pkgBuildZip(tree, zip, NULL, 0, err, sizeof(err)));
         TEST_TRUE(pkgBuildAppended(prog, zip, exe, err, sizeof(err)));
     }
     
    @@ -1014,8 +1162,8 @@ static int32_t readVideoWidth(const char *iniPath) {
     
     // Run an unpack job to its end; msg gets its message, *files (when files
     // is given) a copy of the names it unpacked.
    -static PkgStepE runJob(const PkgSourceT *src, const char *dest, PkgAskDiskFnT ask, PkgKeepFnT keep, void *user, char *msg, char ***files) {
    -    PkgJobT *job  = pkgJobCreate(src, dest, ask, keep, user);
    +static PkgStepE runJob(const PkgSourceT *src, const char *dest, int32_t tier, PkgAskDiskFnT ask, PkgKeepFnT keep, void *user, char *msg, char ***files) {
    +    PkgJobT *job  = pkgJobCreate(src, dest, tier, ask, keep, user);
         PkgStepE step = PkgStepErrorE;
     
         msg[0] = '\0';
    @@ -1152,6 +1300,7 @@ int main(void) {
         caseFind();
         caseInstallAppended();
         caseSplitSwap();
    +    caseTiers();
         caseCrc();
         caseEmptyLists();
         caseCancel();
    diff --git a/src/test/shell/appsTest.c b/src/test/shell/appsTest.c
    index f28789cd..112cc3f8 100644
    --- a/src/test/shell/appsTest.c
    +++ b/src/test/shell/appsTest.c
    @@ -38,8 +38,10 @@
     #include "dvxPlat.h"
     #include "dvxPrefs.h"
     #include "dvxRes.h"
    +#include "dvxText.h"
     #include "taskSwch.h"
     
    +#include "checkbox/checkbox.h"
     #include "dropdown/dropdown.h"
     #include "listBox/listBox.h"
     #include "listView/listView.h"
    @@ -99,6 +101,9 @@
     #define SYS_WHEEL_STEP    5
     #define SYS_STEP_TOO_BIG  99
     #define SYS_BAD_MODE      99
    +#define SYS_BPP_PALETTE   8      // the depth text is never smoothed at
    +#define SYS_TRUE_OTHER    1      // a true that is not BAS_TRUE
    +#define CP_SMOOTH_CAPTION "Smooth the edges of text (more than 256 colors)"   // cpanel.c's checkbox
     #define SYS_THEME         "SYSBAS.THM"
     #define SYS_WALLPAPER     "CONFIG/WPAPER/SAMPLE.BMP"
     #define SYS_SHOT          "SYSSHOT.PNG"   // SysScreenshot's picture
    @@ -1090,6 +1095,25 @@ static void caseCpanelSysBas(void) {
             TEST_EQ(ctx->display.width, origW);
         }
     
    +    // Text is smoothed above 8 bits per pixel only: a mode change applies
    +    // the setting again, which it keeps
    +    int32_t mode8 = 0;
    +
    +    while (mode8 < count && modes[mode8].bpp != SYS_BPP_PALETTE) {
    +        mode8++;
    +    }
    +
    +    TEST_TRUE(mode8 < count);
    +    TEST_TRUE(dvxFontSmooth());
    +
    +    if (mode8 < count) {
    +        TEST_EQ(SysSetVideoMode(modes[mode8].w, modes[mode8].h, SYS_BPP_PALETTE), BAS_TRUE);
    +        TEST_TRUE(!dvxFontSmooth());
    +        TEST_EQ(SysTextSmoothing(), BAS_TRUE);
    +        TEST_EQ(SysSetVideoMode(origW, origH, origBpp), BAS_TRUE);
    +        TEST_TRUE(dvxFontSmooth());
    +    }
    +
         // Colors: ids, names, BASIC RGB values, store / load, themes
         uint8_t r = 0;
         uint8_t g = 0;
    @@ -1214,6 +1238,24 @@ static void caseCpanelSysBas(void) {
         TEST_EQ(SysMouseMickeys(), MOUSE_SPEED_DEFAULT);
         TEST_EQ(SysMouseWheelStep(), MOUSE_WHEEL_STEP_DEFAULT);
     
    +    // Text smoothing: the setting, store / load, on by default
    +    SysSetTextSmoothing(BAS_FALSE);
    +    TEST_EQ(SysTextSmoothing(), BAS_FALSE);
    +    TEST_TRUE(!dvxFontSmooth());
    +    SysFontsStore(snap);
    +    SysFontsStore(0);
    +    TEST_STR_EQ(PrefsGet(snap, "fonts", "smoothing", ""), "false");
    +    SysSetTextSmoothing(SYS_TRUE_OTHER);
    +    TEST_EQ(SysTextSmoothing(), BAS_TRUE);
    +    TEST_TRUE(dvxFontSmooth());
    +    SysFontsLoad(snap);
    +    TEST_EQ(SysTextSmoothing(), BAS_FALSE);
    +    SysFontsLoad(0);
    +    TEST_EQ(SysTextSmoothing(), BAS_FALSE);
    +    SysFontsLoad(empty);
    +    TEST_EQ(SysTextSmoothing(), BAS_TRUE);
    +    TEST_TRUE(dvxFontSmooth());
    +
         // The system font's glyphs (what DVX Demo draws by hand)
         const BitmapFontT *font  = dvxGetFont(tCtx());
         int32_t            glyph = SYS_GLYPH_CHAR - font->firstChar;
    @@ -1287,10 +1329,23 @@ static void caseCpanelVideo(void) {
         fixDialogsOff();
         TEST_TRUE(tCtx()->display.width != origW);
     
    +    // Smooth text: applied at once (put back by Cancel)
    +    WidgetT *smooth = tFindByText(root, CP_SMOOTH_CAPTION);
    +
    +    TEST_TRUE(smooth != NULL && wgtCheckboxIsChecked(smooth));
    +
    +    if (smooth) {
    +        wgtCheckboxSetChecked(smooth, false);
    +        smooth->onChange(smooth);
    +        TEST_TRUE(!tCtx()->fontSmoothing);
    +    }
    +
         // Cancel: restoreSnapshot puts the original mode back.
         clickButton(win, "Cancel");
         fixPumpShell(SETTLE_FRAMES);
         TEST_EQ(tCtx()->display.width, origW);
    +    TEST_TRUE(tCtx()->fontSmoothing);
    +    TEST_TRUE(dvxFontSmooth());
     
         fixKillAllApps();
         fixShellDown();
    diff --git a/src/test/shell/shellTest.c b/src/test/shell/shellTest.c
    index 07d5ac6d..1bf09639 100644
    --- a/src/test/shell/shellTest.c
    +++ b/src/test/shell/shellTest.c
    @@ -31,6 +31,8 @@
     #include "shellFixture.h"
     #include "dvxMem.h"
     #include "dvxPlat.h"
    +#include "dvxRes.h"
    +#include "dvxText.h"
     #include "taskSwch.h"
     
     // The Task Manager DXE is compiled into this suite directly (as loaderTest
    @@ -61,6 +63,9 @@
     #define CLOCK_HELP        "apps/kpunch/clock/clock.hlp"
     #define CPANEL_HELP       "apps/kpunch/cpanel/cpanel.hlp"
     #define HELP_DIR_MODE     0755
    +#define APP_FONTS         "apps/kpunch/clock/fonts.app"           // a copy of the Clock carrying a font
    +#define FONT_FIXTURE      "src/test/tools/golden/fonts/TINY.DFN"  // its font: family Tiny
    +#define FONT_SIZES        2                                       // the sizes it has
     
     
     typedef struct {
    @@ -77,6 +82,7 @@ typedef struct {
     // ============================================================
     
     static void        caseAppCleanupHook(void);
    +static void        caseAppFonts(void);
     static void        caseAppInit(void);
     static void        caseCallbackApps(void);
     static void        caseClock(void);
    @@ -175,6 +181,51 @@ static void caseAppCleanupHook(void) {
     }
     
     
    +// A program's font resources are its own while it runs: added when the
    +// shell loads it, gone when it ends
    +static void caseAppFonts(void) {
    +    tCase("a program's font resources are its own while it runs");
    +    fixShellUp(SCREEN_BPP);
    +
    +    AppContextT *ctx  = tCtx();
    +    int32_t     *prev = dvxMemAppIdPtr;
    +    int32_t      len  = 0;
    +    char         font[DVX_MAX_PATH];
    +
    +    snprintf(font, sizeof(font), "%s/%s", fixRepoRoot, FONT_FIXTURE);
    +
    +    char *dfn = platformReadFile(font, &len);
    +
    +    TEST_TRUE(dfn != NULL);
    +    TEST_TRUE(fixCopyFileTo(APP_CLOCK, APP_FONTS));
    +    TEST_EQ(dvxResAppend(APP_FONTS, "tiny", DVX_RES_FONT, dfn, (uint32_t)len), 0);
    +    free(dfn);
    +
    +#if defined(__SANITIZE_ADDRESS__)
    +    __lsan_disable();
    +#endif
    +    int32_t id = shellLoadApp(ctx, APP_FONTS);
    +    fixPumpShell(SETTLE_FRAMES);
    +#if defined(__SANITIZE_ADDRESS__)
    +    __lsan_enable();
    +#endif
    +
    +    TEST_TRUE(id > 0);
    +    dvxMemAppIdPtr    = &ctx->currentAppId;
    +    ctx->currentAppId = id;
    +    TEST_EQ(dvxFontSizes("Tiny", 0, NULL, 0), FONT_SIZES);
    +    ctx->currentAppId = 0;
    +    TEST_EQ(dvxFontSizes("Tiny", 0, NULL, 0), 0);
    +    shellForceKillApp(ctx, shellGetApp(id));
    +    fixPumpShell(SETTLE_FRAMES);
    +    ctx->currentAppId = id;
    +    TEST_EQ(dvxFontSizes("Tiny", 0, NULL, 0), 0);
    +    ctx->currentAppId = 0;
    +    dvxMemAppIdPtr    = prev;
    +    fixShellDown();
    +}
    +
    +
     static void caseAppInit(void) {
         tCase("shellAppInit seeds the shell slot only");
         fixShellUp(SCREEN_BPP);
    @@ -1280,6 +1331,7 @@ int main(void) {
     
         caseAppInit();
         caseAppCleanupHook();
    +    caseAppFonts();
         caseSymbolPrefix();
         caseShutdownHooksReleaseHeap();
         caseClock();
    diff --git a/src/test/terminal/testTermApp.c b/src/test/terminal/testTermApp.c
    index eb81f36e..a8ab7d5f 100644
    --- a/src/test/terminal/testTermApp.c
    +++ b/src/test/terminal/testTermApp.c
    @@ -106,6 +106,7 @@
     #define MENU_BREAK              "Send &Break"
     #define MENU_SESSION            "&Session Settings..."
     #define MENU_ECHO               "&Local Echo"
    +#define MENU_MUSIC              "ANSI &Music"
     #define MENU_XFER_OPTIONS       "&Transfer Options..."
     #define MENU_SEND               "&Send Files..."
     #define MENU_ABOUT              "&About Terminal..."
    @@ -322,6 +323,20 @@ static void caseConnectAndType(WindowT *win) {
         menu(win, MENU_ECHO);
         TEST_TRUE(fileHas(INI_FILE, "localecho = false"));
     
    +    // ANSI music and the bell from the BBS play without trouble and the
    +    // terminal goes on; ANSI Music is kept in the phone book
    +    peerSay("\x1b[MFT255L64CE\x0e\aOn with it\r\n");
    +    tick(SETTLE_TICKS);
    +    tType("y");
    +    tick(SETTLE_TICKS);
    +    TEST_TRUE(peerSaw("y"));
    +    menu(win, MENU_MUSIC);
    +    TEST_TRUE(fileHas(INI_FILE, "ansimusic = false"));
    +    peerSay("\x1b[NC\x0e");
    +    tick(SETTLE_TICKS);
    +    menu(win, MENU_MUSIC);
    +    TEST_TRUE(fileHas(INI_FILE, "ansimusic = true"));
    +
         // Break holds the line for a moment.
         menu(win, MENU_BREAK);
         TEST_TRUE(loopGetBreak(LOCAL_COM));
    diff --git a/src/test/tools/dfnReadTest.c b/src/test/tools/dfnReadTest.c
    new file mode 100644
    index 00000000..f28561a5
    --- /dev/null
    +++ b/src/test/tools/dfnReadTest.c
    @@ -0,0 +1,585 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// dfnReadTest.c -- the .DFN reader (dvxFontFmt.c) under ASan/UBSan, on
    +// mkfont's output (T9, runTools.sh).
    +//
    +//   dfnReadTest check FONT.DFN...       every strike of every file loads,
    +//                                       and its notice and licence texts
    +//   dfnReadTest dump FONT.DFN           header, texts, strikes and every
    +//                                       glyph present as text ('#' = ink,
    +//                                       ':' and '+' a gray strike's
    +//                                       partial levels)
    +//   dfnReadTest corrupt FONT.DFN DIR    damaged copies (written in DIR)
    +//                                       are refused with the right reason
    +//   dfnReadTest sources FONT.DFN DIR    the font read inside another file
    +//                                       (written in DIR) and from memory
    +//                                       is the font read from its file
    +//
    +// FONT.DFN for corrupt must hold two strikes or more.  Exit status: the
    +// number of failures (2 for usage).
    +
    +#include "dvxFontFmt.h"
    +
    +#include 
    +#include 
    +#include 
    +#include 
    +
    +#define PATH_MAX_LEN       512
    +#define DFN_STRIKE_UNKNOWN 0x8000       // a strike flag no reader knows
    +#define WRAP_BEFORE        1000         // bytes put before a font inside another file
    +#define WRAP_AFTER         300          // and after it
    +#define WRAP_FILL          0xA5
    +#define FNV_OFFSET         2166136261u  // FNV-1a, 32 bits
    +#define FNV_PRIME          16777619u
    +
    +// A damage done to a copy of the file and the reason the reader must give
    +typedef struct {
    +    const char *name;
    +    const char *reason;
    +} CaseT;
    +
    +static int32_t sFailures;
    +
    +// A pixel in a dump, by level: none, gray levels, full ('#' for a 1-bit pixel)
    +static const char sLevelChars[FONT_GRAY_FULL + 1] = { '.', ':', '+', '#' };
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +static int32_t  check(int32_t count, char **paths);
    +static int32_t  corrupt(const char *path, const char *dir);
    +static void     damage(uint8_t *data, int32_t *len, const char *name);
    +static int32_t  dump(const char *path);
    +static void     expectFail(const char *what, bool ok, const char *err, const char *reason);
    +static bool     fingerprint(const DfnSourceT *src, uint32_t *hash, char *err, int32_t errSize);
    +static uint32_t fnv(uint32_t hash, const void *data, size_t len);
    +static uint8_t *readAll(const char *path, int32_t *len);
    +static int32_t  sources(const char *path, const char *dir);
    +static bool     writeAll(const char *path, const uint8_t *data, int32_t len);
    +int             main(int argc, char **argv);
    +
    +
    +static int32_t check(int32_t count, char **paths) {
    +    for (int32_t i = 0; i < count; i++) {
    +        DfnSourceT src;
    +        DfnFileT   f;
    +        char       err[DFN_ERR_MAX];
    +
    +        dfnSourceFile(&src, paths[i]);
    +
    +        bool ok = dfnOpen(&src, &f, err, sizeof(err));
    +
    +        // A notice and a licence go with every font mkfont makes
    +        for (int32_t t = DfnTextNoticeE; ok && t <= DfnTextLicenceE; t++) {
    +            char *text = dfnLoadText(&src, &f, (DfnTextE)t, err, sizeof(err));
    +
    +            ok = text != NULL;
    +
    +            if (ok && !text[0]) {
    +                snprintf(err, sizeof(err), "%s: no %s text", paths[i], t == DfnTextNoticeE ? "notice" : "licence");
    +                ok = false;
    +            }
    +
    +            free(text);
    +        }
    +
    +        for (int32_t s = 0; ok && s < f.header.strikeCount; s++) {
    +            DfnStrikeDataT d;
    +
    +            ok = dfnLoadStrike(&src, &f, s, &d, err, sizeof(err));
    +
    +            // The line holds every glyph's ink
    +            for (int32_t c = 0; ok && c < DFN_CHAR_COUNT; c++) {
    +                const FontGlyphT *g = &d.glyphs[c];
    +
    +                if (g->height > 0 && (g->top > d.info.ascent || g->height - g->top > d.info.descent)) {
    +                    snprintf(err, sizeof(err), "%s: character %02" PRIX32 " passes the line at %" PRId32 " px", paths[i], (uint32_t)c, (int32_t)d.info.pixelSize);
    +                    ok = false;
    +                }
    +            }
    +
    +            if (ok && d.info.ascent + d.info.descent > d.info.lineHeight) {
    +                snprintf(err, sizeof(err), "%s: line height under ascent + descent at %" PRId32 " px", paths[i], (int32_t)d.info.pixelSize);
    +                ok = false;
    +            }
    +
    +            dfnFreeStrike(&d);
    +        }
    +
    +        if (!ok) {
    +            printf("FAIL %s\n", err);
    +            sFailures++;
    +        }
    +    }
    +
    +    printf("check: %" PRId32 " files, %" PRId32 " failures\n", count, sFailures);
    +    return sFailures;
    +}
    +
    +
    +static int32_t corrupt(const char *path, const char *dir) {
    +    static const CaseT cases[] = {
    +        { "empty",          "cannot be read"                         },
    +        { "shortHeader",    "cannot be read"                         },
    +        { "magic",          "is not a DVX font file of this version" },
    +        { "version",        "is not a DVX font file of this version" },
    +        { "noStrikes",      "is damaged"                             },
    +        { "tooManyStrikes", "is damaged"                             },
    +        { "shortStrikes",   "is damaged"                             },
    +        { "familyNoNul",    "is damaged"                             },
    +        { "noticePastEnd",  "is damaged"                             },
    +        { "licencePastEnd", "is damaged"                             },
    +        { "sizesFall",      "is damaged"                             },
    +        { "glyphsPastEnd",  "is damaged"                             },
    +        { "bitmapsPastEnd", "is damaged"                             },
    +        { "truncated",      "is damaged"                             },
    +        { "grayAlone",      "is damaged"                             },
    +        { "unknownFlag",    "is damaged"                             }
    +    };
    +    int32_t    len  = 0;
    +    uint8_t   *good = readAll(path, &len);
    +    char       out[PATH_MAX_LEN];
    +    char       err[DFN_ERR_MAX];
    +    DfnFileT   f;
    +    DfnSourceT src;
    +
    +    if (!good) {
    +        printf("FAIL cannot read %s\n", path);
    +        return 1;
    +    }
    +
    +    if (len < (int32_t)sizeof(DfnHeaderT) || ((const DfnHeaderT *)good)->strikeCount < 2) {
    +        printf("FAIL %s does not hold two strikes\n", path);
    +        free(good);
    +        return 1;
    +    }
    +
    +    for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
    +        uint8_t *copy    = (uint8_t *)malloc((size_t)len);
    +        int32_t  copyLen = len;
    +
    +        memcpy(copy, good, (size_t)len);
    +        damage(copy, ©Len, cases[i].name);
    +        snprintf(out, sizeof(out), "%s/%s.dfn", dir, cases[i].name);
    +        dfnSourceFile(&src, out);
    +
    +        if (!writeAll(out, copy, copyLen)) {
    +            printf("FAIL cannot write %s\n", out);
    +            sFailures++;
    +        } else {
    +            expectFail(cases[i].name, dfnOpen(&src, &f, err, sizeof(err)), err, cases[i].reason);
    +        }
    +
    +        free(copy);
    +    }
    +
    +    // A file that opens but whose glyph sizes do not add up to its
    +    // bitmaps: dfnLoadStrike refuses it
    +    DfnFileT       good2;
    +    DfnStrikeDataT s;
    +    DfnStrikeT    *first = (DfnStrikeT *)(good + sizeof(DfnHeaderT));
    +    FontGlyphT    *g     = (FontGlyphT *)(good + first->glyphOffset);
    +
    +    for (int32_t c = 0; c < DFN_CHAR_COUNT; c++) {
    +        if (g[c].height > 0) {
    +            g[c].height++;
    +            break;
    +        }
    +    }
    +
    +    snprintf(out, sizeof(out), "%s/glyphSizes.dfn", dir);
    +    dfnSourceFile(&src, out);
    +
    +    if (writeAll(out, good, len) && dfnOpen(&src, &good2, err, sizeof(err))) {
    +        expectFail("glyphSizes", dfnLoadStrike(&src, &good2, 0, &s, err, sizeof(err)), err, "has glyphs that do not match its bitmaps");
    +
    +        if (s.bitmaps) {
    +            printf("FAIL glyphSizes: a refused strike keeps its bitmaps\n");
    +            sFailures++;
    +        }
    +
    +        // One below the first strike and one past the last
    +        expectFail("strikeBelow", dfnLoadStrike(&src, &good2, -1, &s, err, sizeof(err)), err, "no such size");
    +        expectFail("strikePast", dfnLoadStrike(&src, &good2, good2.header.strikeCount, &s, err, sizeof(err)), err, "no such size");
    +
    +        // The file gone between open and load
    +        remove(out);
    +        expectFail("goneAtLoad", dfnLoadStrike(&src, &good2, 0, &s, err, sizeof(err)), err, "cannot be opened");
    +        expectFail("goneAtText", dfnLoadText(&src, &good2, DfnTextLicenceE, err, sizeof(err)) != NULL, err, "cannot be read");
    +    } else {
    +        printf("FAIL glyphSizes: %s did not open\n", out);
    +        sFailures++;
    +    }
    +
    +    snprintf(out, sizeof(out), "%s/noSuchFile.dfn", dir);
    +    dfnSourceFile(&src, out);
    +    expectFail("missing", dfnOpen(&src, &f, err, sizeof(err)), err, "cannot be opened");
    +
    +    // No room for a reason: still refused, nothing written
    +    if (dfnOpen(&src, &f, NULL, 0)) {
    +        printf("FAIL missing (no err buffer) opened\n");
    +        sFailures++;
    +    }
    +
    +    memset(&s, 0, sizeof(s));
    +    dfnFreeStrike(&s);
    +    free(good);
    +    printf("corrupt: %" PRId32 " failures\n", sFailures);
    +    return sFailures;
    +}
    +
    +
    +// Damage name done to data (len bytes, which it may shorten)
    +static void damage(uint8_t *data, int32_t *len, const char *name) {
    +    DfnHeaderT *h      = (DfnHeaderT *)data;
    +    DfnStrikeT *s      = (DfnStrikeT *)(data + sizeof(DfnHeaderT));
    +    DfnStrikeT  second = s[1];
    +
    +    if (strcmp(name, "empty") == 0) {
    +        *len = 0;
    +    } else if (strcmp(name, "shortHeader") == 0) {
    +        *len = (int32_t)sizeof(DfnHeaderT) / 2;
    +    } else if (strcmp(name, "magic") == 0) {
    +        h->magic[0] ^= 1;
    +    } else if (strcmp(name, "version") == 0) {
    +        h->version = DFN_VERSION + 1;
    +    } else if (strcmp(name, "noStrikes") == 0) {
    +        h->strikeCount = 0;
    +    } else if (strcmp(name, "tooManyStrikes") == 0) {
    +        h->strikeCount = DFN_STRIKE_MAX + 1;
    +    } else if (strcmp(name, "shortStrikes") == 0) {
    +        *len = (int32_t)(sizeof(DfnHeaderT) + sizeof(DfnStrikeT));
    +    } else if (strcmp(name, "familyNoNul") == 0) {
    +        memset(h->family, 'F', sizeof(h->family));
    +    } else if (strcmp(name, "noticePastEnd") == 0) {
    +        h->noticeBytes = (uint32_t)*len;
    +    } else if (strcmp(name, "licencePastEnd") == 0) {
    +        h->licenceOffset = (uint32_t)*len + 1;
    +    } else if (strcmp(name, "sizesFall") == 0) {
    +        s[1] = s[0];
    +        s[0] = second;
    +    } else if (strcmp(name, "glyphsPastEnd") == 0) {
    +        s[0].glyphOffset = (uint32_t)*len;
    +    } else if (strcmp(name, "bitmapsPastEnd") == 0) {
    +        s[0].bitmapBytes = (uint32_t)*len;
    +    } else if (strcmp(name, "truncated") == 0) {
    +        (*len)--;
    +    } else if (strcmp(name, "grayAlone") == 0) {
    +        s[0].flags = DFN_STRIKE_GRAY2;
    +    } else if (strcmp(name, "unknownFlag") == 0) {
    +        s[0].flags = DFN_STRIKE_UNKNOWN;
    +    }
    +}
    +
    +
    +// The file as text: its header, then per strike its metrics and every
    +// glyph present with its record and its bitmap
    +static int32_t dump(const char *path) {
    +    DfnSourceT src;
    +    DfnFileT   f;
    +    char       err[DFN_ERR_MAX];
    +
    +    dfnSourceFile(&src, path);
    +
    +    if (!dfnOpen(&src, &f, err, sizeof(err))) {
    +        printf("FAIL %s\n", err);
    +        return 1;
    +    }
    +
    +    printf("family '%s' style %" PRIu32 " flags %" PRIu32 " strikes %" PRIu32 "\n", f.header.family, (uint32_t)f.header.style, (uint32_t)f.header.faceFlags, (uint32_t)f.header.strikeCount);
    +
    +    // The texts, a line at a time ("|" ends each line; CR LF shown as "\r")
    +    for (int32_t t = DfnTextNoticeE; t <= DfnTextLicenceE; t++) {
    +        char *text = dfnLoadText(&src, &f, (DfnTextE)t, err, sizeof(err));
    +
    +        if (!text) {
    +            printf("FAIL %s\n", err);
    +            return 1;
    +        }
    +
    +        printf("%s %" PRIu32 " bytes:\n", t == DfnTextNoticeE ? "notice" : "licence", (uint32_t)strlen(text));
    +
    +        for (const char *p = text; *p; ) {
    +            const char *eol = strchr(p, '\n');
    +            int32_t     n   = eol ? (int32_t)(eol - p) : (int32_t)strlen(p);
    +            bool        cr  = n > 0 && p[n - 1] == '\r';
    +
    +            printf("  %.*s%s|\n", (int)(cr ? n - 1 : n), p, cr ? "\\r" : "");
    +            p += n + (eol ? 1 : 0);
    +        }
    +
    +        free(text);
    +    }
    +
    +    for (int32_t i = 0; i < f.header.strikeCount; i++) {
    +        DfnStrikeDataT s;
    +
    +        if (!dfnLoadStrike(&src, &f, i, &s, err, sizeof(err))) {
    +            printf("FAIL %s\n", err);
    +            return 1;
    +        }
    +
    +        bool gray = (s.info.flags & DFN_STRIKE_GRAY2) != 0;
    +
    +        printf("strike %" PRId32 " px%s: ascent %" PRId32 " descent %" PRId32 " line %" PRId32 " widest %" PRIu32 " bitmaps %" PRIu32 "\n", (int32_t)s.info.pixelSize, gray ? " gray" : "", (int32_t)s.info.ascent, (int32_t)s.info.descent, (int32_t)s.info.lineHeight, (uint32_t)s.info.maxAdvance, s.info.bitmapBytes);
    +
    +        for (int32_t c = 0; c < DFN_CHAR_COUNT; c++) {
    +            const FontGlyphT *g = &s.glyphs[c];
    +
    +            if (g->flags & FONT_GLYPH_MISSING) {
    +                continue;
    +            }
    +
    +            printf("  %02" PRIX32 ": advance %" PRIu32 " size %" PRIu32 "x%" PRIu32 " at %" PRId32 ",%" PRId32 "\n", (uint32_t)c, (uint32_t)g->advance, (uint32_t)g->width, (uint32_t)g->height, (int32_t)g->left, (int32_t)g->top);
    +
    +            const uint8_t *rows     = s.bitmaps + s.bitmapAt[c];
    +            int32_t        rowBytes = FONT_ROW_BYTES_BPP(g->width, gray ? FONT_GRAY_BPP : 1);
    +
    +            for (int32_t y = 0; y < g->height; y++) {
    +                const uint8_t *row = rows + y * rowBytes;
    +
    +                fputs("    ", stdout);
    +
    +                for (int32_t x = 0; x < g->width; x++) {
    +                    int32_t level = gray ? FONT_GRAY_LEVEL(row, x) : ((row[FONT_PIXEL_BYTE(x)] & FONT_PIXEL_BIT(x)) ? FONT_GRAY_FULL : 0);
    +
    +                    putchar(sLevelChars[level]);
    +                }
    +
    +                putchar('\n');
    +            }
    +        }
    +
    +        dfnFreeStrike(&s);
    +    }
    +
    +    return 0;
    +}
    +
    +
    +static void expectFail(const char *what, bool ok, const char *err, const char *reason) {
    +    if (ok) {
    +        printf("FAIL %s: accepted\n", what);
    +        sFailures++;
    +    } else if (!strstr(err, reason)) {
    +        printf("FAIL %s: '%s' does not say '%s'\n", what, err, reason);
    +        sFailures++;
    +    } else {
    +        printf("ok %s: %s\n", what, reason);
    +    }
    +}
    +
    +
    +// A hash of everything the font at src holds as the reader gives it: the
    +// header, the strike table, every strike's glyphs and bitmaps, and both
    +// texts; false with the reason in err when any of it does not load
    +static bool fingerprint(const DfnSourceT *src, uint32_t *hash, char *err, int32_t errSize) {
    +    DfnFileT f;
    +
    +    if (!dfnOpen(src, &f, err, errSize)) {
    +        return false;
    +    }
    +
    +    *hash = fnv(FNV_OFFSET, &f.header, sizeof(f.header));
    +    *hash = fnv(*hash, f.strikes, sizeof(f.strikes[0]) * (size_t)f.header.strikeCount);
    +
    +    for (int32_t i = 0; i < f.header.strikeCount; i++) {
    +        DfnStrikeDataT s;
    +
    +        if (!dfnLoadStrike(src, &f, i, &s, err, errSize)) {
    +            return false;
    +        }
    +
    +        *hash = fnv(*hash, s.glyphs, sizeof(s.glyphs));
    +        *hash = fnv(*hash, s.bitmaps, s.info.bitmapBytes);
    +        dfnFreeStrike(&s);
    +    }
    +
    +    for (int32_t t = DfnTextNoticeE; t <= DfnTextLicenceE; t++) {
    +        char *text = dfnLoadText(src, &f, (DfnTextE)t, err, errSize);
    +
    +        if (!text) {
    +            return false;
    +        }
    +
    +        *hash = fnv(*hash, text, strlen(text));
    +        free(text);
    +    }
    +
    +    return true;
    +}
    +
    +
    +static uint32_t fnv(uint32_t hash, const void *data, size_t len) {
    +    const uint8_t *p = (const uint8_t *)data;
    +
    +    for (size_t i = 0; i < len; i++) {
    +        hash = (hash ^ p[i]) * FNV_PRIME;
    +    }
    +
    +    return hash;
    +}
    +
    +
    +// The whole file (malloc'd), or NULL
    +static uint8_t *readAll(const char *path, int32_t *len) {
    +    FILE *fp = fopen(path, "rb");
    +
    +    if (!fp) {
    +        return NULL;
    +    }
    +
    +    fseek(fp, 0, SEEK_END);
    +    *len = (int32_t)ftell(fp);
    +    fseek(fp, 0, SEEK_SET);
    +
    +    uint8_t *data = (uint8_t *)malloc((size_t)*len);
    +
    +    if (data && fread(data, 1, (size_t)*len, fp) != (size_t)*len) {
    +        free(data);
    +        data = NULL;
    +    }
    +
    +    fclose(fp);
    +    return data;
    +}
    +
    +
    +// The font at path read three ways -- its file, inside another file (as
    +// an application's font resource is), and from memory -- gives the same
    +// font; a font said to run past its file is refused
    +static int32_t sources(const char *path, const char *dir) {
    +    int32_t    len  = 0;
    +    uint8_t   *good = readAll(path, &len);
    +    uint8_t   *wrap = good ? (uint8_t *)malloc((size_t)len + WRAP_BEFORE + WRAP_AFTER) : NULL;
    +    char       out[PATH_MAX_LEN];
    +    char       err[DFN_ERR_MAX];
    +    uint32_t   want = 0;
    +    uint32_t   got  = 0;
    +    DfnSourceT src;
    +    DfnFileT   f;
    +
    +    if (!wrap) {
    +        printf("FAIL cannot read %s\n", path);
    +        free(good);
    +        return 1;
    +    }
    +
    +    memset(wrap, WRAP_FILL, (size_t)len + WRAP_BEFORE + WRAP_AFTER);
    +    memcpy(wrap + WRAP_BEFORE, good, (size_t)len);
    +    snprintf(out, sizeof(out), "%s/wrapped.bin", dir);
    +
    +    if (!writeAll(out, wrap, len + WRAP_BEFORE + WRAP_AFTER)) {
    +        printf("FAIL cannot write %s\n", out);
    +        sFailures++;
    +    }
    +
    +    dfnSourceFile(&src, path);
    +
    +    if (!fingerprint(&src, &want, err, sizeof(err))) {
    +        printf("FAIL %s\n", err);
    +        sFailures++;
    +    }
    +
    +    // Inside another file, at its offset and length
    +    dfnSourceFile(&src, out);
    +    src.base   = WRAP_BEFORE;
    +    src.length = (uint32_t)len;
    +
    +    if (!fingerprint(&src, &got, err, sizeof(err)) || got != want) {
    +        printf("FAIL inside a file: %s\n", got != want ? "not the same font" : err);
    +        sFailures++;
    +    } else {
    +        printf("ok inside a file\n");
    +    }
    +
    +    // In memory
    +    memset(&src, 0, sizeof(src));
    +    src.name   = "memory";
    +    src.data   = good;
    +    src.length = (uint32_t)len;
    +
    +    if (!fingerprint(&src, &got, err, sizeof(err)) || got != want) {
    +        printf("FAIL in memory: %s\n", got != want ? "not the same font" : err);
    +        sFailures++;
    +    } else {
    +        printf("ok in memory\n");
    +    }
    +
    +    // Past the end of its file: refused, never read past
    +    dfnSourceFile(&src, out);
    +    src.base   = (uint32_t)(len + WRAP_BEFORE + WRAP_AFTER + 1);
    +    expectFail("baseOutside", dfnOpen(&src, &f, err, sizeof(err)), err, "cannot be opened");
    +    src.base   = WRAP_BEFORE;
    +    src.length = (uint32_t)(len + WRAP_AFTER + 1);
    +    expectFail("lengthOutside", dfnOpen(&src, &f, err, sizeof(err)), err, "cannot be opened");
    +
    +    // A memory font cut short
    +    memset(&src, 0, sizeof(src));
    +    src.name   = "memory";
    +    src.data   = good;
    +    src.length = (uint32_t)len - 1;
    +    expectFail("memoryShort", dfnOpen(&src, &f, err, sizeof(err)), err, "is damaged");
    +
    +    free(wrap);
    +    free(good);
    +    printf("sources: %" PRId32 " failures\n", sFailures);
    +    return sFailures;
    +}
    +
    +
    +static bool writeAll(const char *path, const uint8_t *data, int32_t len) {
    +    FILE *fp = fopen(path, "wb");
    +
    +    if (!fp) {
    +        return false;
    +    }
    +
    +    bool ok = fwrite(data, 1, (size_t)len, fp) == (size_t)len;
    +
    +    return fclose(fp) == 0 && ok;
    +}
    +
    +
    +int main(int argc, char **argv) {
    +    if (argc >= 3 && strcmp(argv[1], "check") == 0) {
    +        return check(argc - 2, argv + 2);
    +    }
    +
    +    if (argc == 3 && strcmp(argv[1], "dump") == 0) {
    +        return dump(argv[2]);
    +    }
    +
    +    if (argc == 4 && strcmp(argv[1], "corrupt") == 0) {
    +        return corrupt(argv[2], argv[3]);
    +    }
    +
    +    if (argc == 4 && strcmp(argv[1], "sources") == 0) {
    +        return sources(argv[2], argv[3]);
    +    }
    +
    +    fprintf(stderr, "usage: dfnReadTest check FONT.DFN...\n       dfnReadTest dump FONT.DFN\n       dfnReadTest corrupt FONT.DFN DIR\n       dfnReadTest sources FONT.DFN DIR\n");
    +    return 2;
    +}
    diff --git a/src/test/tools/fixtures/fonts/fonts.ini b/src/test/tools/fixtures/fonts/fonts.ini
    new file mode 100644
    index 00000000..d003f104
    --- /dev/null
    +++ b/src/test/tools/fixtures/fonts/fonts.ini
    @@ -0,0 +1,39 @@
    +; fonts.ini -- T9 fixture manifest for mkfont (src/test/tools/runTools.sh)
    +;
    +; TINY: two bitmap sources listed largest first (mkfont sorts them), one
    +; Unicode and one code page 437, bold, box drawing by DVX.  TINYF: the
    +; fixed-pitch source alone.  TINYS: an outline face (DejaVu Sans from
    +; toolchains/fonts) at three sizes, autohinted, 12 px also smoothed.
    +; LIC is made of two licence texts (the runner writes them, one of them
    +; UTF-8).
    +
    +[fonts]
    +TINY  = Tiny
    +TINYF = Tiny Fixed
    +TINYS = Tiny Outline
    +
    +[licences]
    +LIC    = lic1.txt lic2.txt
    +DEJAVU = dejavu/dejavu-fonts-ttf-2.37/LICENSE
    +
    +[TINY]
    +style    = bold
    +licence  = LIC
    +boxdraw  = true
    +source   = tiny437.bdf
    +encoding = cp437
    +source1  = tiny.bdf
    +
    +[TINYF]
    +licence  = LIC
    +source   = tiny437.bdf
    +sizes    = native
    +encoding = cp437
    +
    +[TINYS]
    +style    = italic
    +licence  = DEJAVU
    +source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans-Oblique.ttf
    +sizes    = 12 9 16
    +hinting  = auto
    +smooth   = 12
    diff --git a/src/test/tools/fixtures/fonts/tiny.bdf b/src/test/tools/fixtures/fonts/tiny.bdf
    new file mode 100644
    index 00000000..50b4be67
    --- /dev/null
    +++ b/src/test/tools/fixtures/fonts/tiny.bdf
    @@ -0,0 +1,78 @@
    +STARTFONT 2.1
    +COMMENT tiny.bdf -- T9 fixture for mkfont: a proportional Unicode face,
    +COMMENT 8 px.  Space and A are ordinary glyphs, C cedilla is drawn as
    +COMMENT nothing (mkfont marks it missing), U+2500 and U+2591 sit at
    +COMMENT CP437 C4 and B0, every other character is absent.
    +FONT -dvx-tiny-medium-r-normal--8-80-75-75-p-50-iso10646-1
    +SIZE 8 75 75
    +FONTBOUNDINGBOX 6 8 0 -2
    +STARTPROPERTIES 7
    +PIXEL_SIZE 8
    +FONT_ASCENT 6
    +FONT_DESCENT 2
    +CHARSET_REGISTRY "ISO10646"
    +CHARSET_ENCODING "1"
    +COPYRIGHT "Tiny test face, public domain"
    +DEFAULT_CHAR 32
    +ENDPROPERTIES
    +CHARS 5
    +STARTCHAR space
    +ENCODING 32
    +SWIDTH 500 0
    +DWIDTH 4 0
    +BBX 1 1 0 0
    +BITMAP
    +00
    +ENDCHAR
    +STARTCHAR A
    +ENCODING 65
    +SWIDTH 750 0
    +DWIDTH 6 0
    +BBX 5 6 0 0
    +BITMAP
    +20
    +50
    +88
    +F8
    +88
    +88
    +ENDCHAR
    +STARTCHAR Ccedilla
    +ENCODING 199
    +SWIDTH 750 0
    +DWIDTH 6 0
    +BBX 5 8 0 -2
    +BITMAP
    +00
    +00
    +00
    +00
    +00
    +00
    +00
    +00
    +ENDCHAR
    +STARTCHAR boxH
    +ENCODING 9472
    +SWIDTH 750 0
    +DWIDTH 6 0
    +BBX 6 1 0 2
    +BITMAP
    +FC
    +ENDCHAR
    +STARTCHAR shadeLight
    +ENCODING 9617
    +SWIDTH 750 0
    +DWIDTH 6 0
    +BBX 6 8 0 -2
    +BITMAP
    +88
    +20
    +88
    +20
    +88
    +20
    +88
    +20
    +ENDCHAR
    +ENDFONT
    diff --git a/src/test/tools/fixtures/fonts/tiny437.bdf b/src/test/tools/fixtures/fonts/tiny437.bdf
    new file mode 100644
    index 00000000..bd66ab51
    --- /dev/null
    +++ b/src/test/tools/fixtures/fonts/tiny437.bdf
    @@ -0,0 +1,61 @@
    +STARTFONT 2.1
    +COMMENT tiny437.bdf -- T9 fixture for mkfont: a fixed-pitch face numbered
    +COMMENT in code page 437 (encoding = cp437), 10 px: 01 (smiley), space,
    +COMMENT A and C4 (horizontal line), every other character absent.
    +FONT -dvx-tiny-medium-r-normal--10-100-75-75-c-60-ibm-437
    +SIZE 10 75 75
    +FONTBOUNDINGBOX 6 10 0 -2
    +STARTPROPERTIES 7
    +PIXEL_SIZE 10
    +FONT_ASCENT 8
    +FONT_DESCENT 2
    +CHARSET_REGISTRY "ibm"
    +CHARSET_ENCODING "437"
    +COPYRIGHT "Tiny test face, public domain"
    +DEFAULT_CHAR 32
    +ENDPROPERTIES
    +CHARS 4
    +STARTCHAR smiley
    +ENCODING 1
    +SWIDTH 600 0
    +DWIDTH 6 0
    +BBX 6 6 0 1
    +BITMAP
    +78
    +84
    +CC
    +84
    +B4
    +78
    +ENDCHAR
    +STARTCHAR space
    +ENCODING 32
    +SWIDTH 600 0
    +DWIDTH 6 0
    +BBX 1 1 0 0
    +BITMAP
    +00
    +ENDCHAR
    +STARTCHAR A
    +ENCODING 65
    +SWIDTH 600 0
    +DWIDTH 6 0
    +BBX 5 7 0 0
    +BITMAP
    +20
    +50
    +88
    +88
    +F8
    +88
    +88
    +ENDCHAR
    +STARTCHAR boxH
    +ENCODING 196
    +SWIDTH 600 0
    +DWIDTH 6 0
    +BBX 6 1 0 3
    +BITMAP
    +FC
    +ENDCHAR
    +ENDFONT
    diff --git a/src/test/tools/golden/fonts/TINY.DFN b/src/test/tools/golden/fonts/TINY.DFN
    new file mode 100644
    index 00000000..c97379be
    Binary files /dev/null and b/src/test/tools/golden/fonts/TINY.DFN differ
    diff --git a/src/test/tools/golden/fonts/TINY.dump b/src/test/tools/golden/fonts/TINY.dump
    new file mode 100644
    index 00000000..758386d3
    --- /dev/null
    +++ b/src/test/tools/golden/fonts/TINY.dump
    @@ -0,0 +1,52 @@
    +family 'Tiny' style 1 flags 2 strikes 2
    +notice 89 bytes:
    +  Tiny Bold, converted for DVX from tiny437.bdf, tiny.bdf.\r|
    +  Tiny test face, public domain\r|
    +licence 150 bytes:
    +  "Tiny" licence - (C) 2026 AEthelred\r|
    +  Second line\r|
    +  \r|
    +  ------------------------------------------------------------------------\r|
    +  \r|
    +  Plain ASCII licence.\r|
    +strike 8 px: ascent 6 descent 2 line 8 widest 6 bitmaps 16
    +  20: advance 4 size 1x1 at 0,1
    +    .
    +  41: advance 6 size 5x6 at 0,6
    +    ..#..
    +    .#.#.
    +    #...#
    +    #####
    +    #...#
    +    #...#
    +  B0: advance 6 size 6x8 at 0,6
    +    #...#.
    +    ..#...
    +    #...#.
    +    ..#...
    +    #...#.
    +    ..#...
    +    #...#.
    +    ..#...
    +  C4: advance 6 size 6x1 at 0,3
    +    ######
    +strike 10 px: ascent 8 descent 2 line 10 widest 6 bitmaps 15
    +  01: advance 6 size 6x6 at 0,7
    +    .####.
    +    #....#
    +    ##..##
    +    #....#
    +    #.##.#
    +    .####.
    +  20: advance 6 size 1x1 at 0,1
    +    .
    +  41: advance 6 size 5x7 at 0,7
    +    ..#..
    +    .#.#.
    +    #...#
    +    #...#
    +    #####
    +    #...#
    +    #...#
    +  C4: advance 6 size 6x1 at 0,4
    +    ######
    diff --git a/src/test/tools/golden/fonts/TINYF.DFN b/src/test/tools/golden/fonts/TINYF.DFN
    new file mode 100644
    index 00000000..605877b4
    Binary files /dev/null and b/src/test/tools/golden/fonts/TINYF.DFN differ
    diff --git a/src/test/tools/golden/fonts/TINYF.dump b/src/test/tools/golden/fonts/TINYF.dump
    new file mode 100644
    index 00000000..5ee18b3c
    --- /dev/null
    +++ b/src/test/tools/golden/fonts/TINYF.dump
    @@ -0,0 +1,31 @@
    +family 'Tiny Fixed' style 0 flags 1 strikes 1
    +notice 88 bytes:
    +  Tiny Fixed Regular, converted for DVX from tiny437.bdf.\r|
    +  Tiny test face, public domain\r|
    +licence 150 bytes:
    +  "Tiny" licence - (C) 2026 AEthelred\r|
    +  Second line\r|
    +  \r|
    +  ------------------------------------------------------------------------\r|
    +  \r|
    +  Plain ASCII licence.\r|
    +strike 10 px: ascent 8 descent 2 line 10 widest 6 bitmaps 15
    +  01: advance 6 size 6x6 at 0,7
    +    .####.
    +    #....#
    +    ##..##
    +    #....#
    +    #.##.#
    +    .####.
    +  20: advance 6 size 1x1 at 0,1
    +    .
    +  41: advance 6 size 5x7 at 0,7
    +    ..#..
    +    .#.#.
    +    #...#
    +    #...#
    +    #####
    +    #...#
    +    #...#
    +  C4: advance 6 size 6x1 at 0,4
    +    ######
    diff --git a/src/test/tools/golden/fonts/TINYS.DFN b/src/test/tools/golden/fonts/TINYS.DFN
    new file mode 100644
    index 00000000..432db5d7
    Binary files /dev/null and b/src/test/tools/golden/fonts/TINYS.DFN differ
    diff --git a/src/test/tools/golden/icons/mkwgticon_richedit.bmp b/src/test/tools/golden/icons/mkwgticon_richedit.bmp
    new file mode 100644
    index 00000000..1894a973
    --- /dev/null
    +++ b/src/test/tools/golden/icons/mkwgticon_richedit.bmp
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:6a76a5e9c931cedc2849b7b73f6eda562289c15d24a3ea3130e7cf24d204ded3
    +size 1782
    diff --git a/src/test/tools/golden/icons/mkwgticon_richlabel.bmp b/src/test/tools/golden/icons/mkwgticon_richlabel.bmp
    new file mode 100644
    index 00000000..98e44597
    --- /dev/null
    +++ b/src/test/tools/golden/icons/mkwgticon_richlabel.bmp
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:dc379786fcd8779c9d61efe3f4613fac3eb00566ad2e81309183fc81dd8dea83
    +size 1782
    diff --git a/src/test/tools/runTools.sh b/src/test/tools/runTools.sh
    index 095ee974..87be54ef 100755
    --- a/src/test/tools/runTools.sh
    +++ b/src/test/tools/runTools.sh
    @@ -2,9 +2,11 @@
     # runTools.sh -- suite T9: host tool regression tests.
     #
     # Covers dvxhlpc (CLI + library mode), dvxres, mkicon, mktbicon,
    -# mkwgticon, bmp2raw and mksounds.  Outputs are compared byte-for-byte against
    -# golden/.  Set UPDATE_GOLDENS=1 to regenerate the goldens.  Exits
    -# non-zero if any check fails.
    +# mkwgticon, bmp2raw, mksounds, and mkfont (with the .DFN reader).
    +# Outputs are compared byte-for-byte against golden/.  mkfont's tests
    +# also use fonts from FONT_SOURCES (src/common.mk; installToolchain.sh
    +# puts them in toolchains/fonts).  Set UPDATE_GOLDENS=1 to regenerate the
    +# goldens.  Exits non-zero if any check fails.
     #
     #   src/test/tools/runTools.sh
     #   UPDATE_GOLDENS=1 src/test/tools/runTools.sh
    @@ -39,8 +41,51 @@ else
     fi
     INCLUDES=(-I"$TOOLS" -I"$LIBDVX" -I"$LIBDVX/platform")
     PLATFORM_UTIL="$LIBDVX/platform/dvxPlatformUtil.c"
    +# FreeType and the fonts mkfont converts, where the build takes them
    +# (the $(...) are make's, expanded by make)
    +# shellcheck disable=SC2016
    +read -r FREETYPE FONT_SOURCES < <(env -u MAKEFLAGS -u MFLAGS -u MAKELEVEL make -s --no-print-directory -f "$ROOT/src/common.mk" --eval 'printFonts: ; @echo $(FREETYPE_DIR) $(FONT_SOURCES)' printFonts)
     STB_DS_IMPL="$LIBDVX/thirdparty/stb_ds_impl.c"
     
    +# toolSrcs VAR NAME -- array NAME set to VAR as src/tools/Makefile
    +# defines it, each word made absolute, so the builds below link what the
    +# product links and no source list is copied here.  A list that comes
    +# back empty (a renamed variable) stops the run.
    +toolSrcs() {
    +    local -n srcs="$2"
    +    local    word
    +
    +    srcs=()
    +    for word in $(env -u MAKEFLAGS -u MFLAGS -u MAKELEVEL make -s --no-print-directory -C "$TOOLS" --eval "t9PrintVar: ; @echo \$($1)" t9PrintVar 2> /dev/null); do
    +        case "$word" in
    +            /*) srcs+=("$word") ;;
    +            *)  srcs+=("$(realpath -m "$TOOLS/$word")") ;;
    +        esac
    +    done
    +    if [ "${#srcs[@]}" -eq 0 ]; then
    +        echo "runTools.sh: src/tools/Makefile defines no $1" >&2
    +        exit 1
    +    fi
    +}
    +
    +# (declared here so the names are visible where toolSrcs fills them)
    +declare -a bmp2rawSrcs
    +declare -a dvxhlpcSrcs
    +declare -a dvxresSrcs
    +declare -a mkfontSrcs
    +declare -a mkiconSrcs
    +declare -a mksoundsSrcs
    +declare -a mktbiconSrcs
    +declare -a mkwgticonSrcs
    +toolSrcs BMP2RAW_SRCS   bmp2rawSrcs
    +toolSrcs DVXHLPC_SRCS   dvxhlpcSrcs
    +toolSrcs DVXRES_SRCS    dvxresSrcs
    +toolSrcs MKFONT_SRCS    mkfontSrcs
    +toolSrcs MKICON_SRCS    mkiconSrcs
    +toolSrcs MKSOUNDS_SRCS  mksoundsSrcs
    +toolSrcs MKTBICON_SRCS  mktbiconSrcs
    +toolSrcs MKWGTICON_SRCS mkwgticonSrcs
    +
     HLP_HEADER_SIZE=60
     HLP_MAGIC=1212960836
     HLP_VERSION=1
    @@ -50,7 +95,7 @@ MKICON_TYPES="noicon clock notepad cpanel dvxdemo imgview basic help iconed rese
     MKTBICON_TYPES="open save run stop code design debug stepinto stepover stepout runtocur"
     MKSOUNDS_NAMES="beep error exit info question startup warning winclose winopen"
     MKSOUNDS_MAX_TOTAL=102400
    -MKWGTICON_TYPES="button label textbox checkbox radio dropdown combobox listbox listview treeview image imgbtn canvas slider spinner progress timer frame hbox vbox splitter scrollpane tabctrl toolbar statusbar separator spacer terminal wrapbox datactrl dbgrid"
    +MKWGTICON_TYPES="button label textbox checkbox radio dropdown combobox listbox listview treeview image imgbtn canvas slider spinner progress timer frame hbox vbox splitter scrollpane tabctrl toolbar statusbar separator spacer terminal wrapbox richedit richlabel datactrl dbgrid"
     
     passCount=0
     failCount=0
    @@ -167,7 +212,7 @@ else
         exit 1
     fi
     
    -for t in dvxhlpc dvxres mkicon mktbicon mkwgticon bmp2raw mksounds; do
    +for t in dvxhlpc dvxres mkicon mktbicon mkwgticon bmp2raw mksounds mkfont; do
         check "host tool $t exists" test -x "$HOST/$t"
     done
     
    @@ -618,7 +663,21 @@ expectRc "dvxres add 32-char name fails" 1 $?
     expectGrep "dvxres add long name message" "Resource name too long (max 31 chars)" "$WORK/resLongName.txt"
     "$HOST/dvxres" add "$MOD" nofile binary noSuchFile.bin > /dev/null 2> "$WORK/resNoFile.txt"
     expectRc "dvxres add missing data file fails" 1 $?
    -expectGrep "dvxres add missing data file message" "Cannot open or read data file: noSuchFile.bin" "$WORK/resNoFile.txt"
    +expectGrep "dvxres add missing data file message" "cannot open or read noSuchFile.bin" "$WORK/resNoFile.txt"
    +
    +# font: a .DFN (the fixture font's golden), checked to be one
    +"$HOST/dvxres" add "$MOD" tiny font "$GOLD/fonts/TINY.DFN" > "$WORK/resFont.txt" 2>&1
    +expectRc "dvxres add font" 0 $?
    +expectGrep "dvxres add font summary" "Added 'tiny' (font, " "$WORK/resFont.txt"
    +"$HOST/dvxres" list "$MOD" > "$WORK/resFontList.txt" 2>&1
    +check "dvxres lists a font resource" grep -Eq "^  tiny +font " "$WORK/resFontList.txt"
    +"$HOST/dvxres" add "$MOD" notfont font blob.bin > /dev/null 2> "$WORK/resNotFont.txt"
    +expectRc "dvxres add a non-font as a font fails" 1 $?
    +expectGrep "dvxres add a non-font as a font message" "blob.bin is not a DVX font file of this version." "$WORK/resNotFont.txt"
    +printf 'f font %s\n' "blob.bin" > "$WORK/badFont.res"
    +"$HOST/dvxres" build "$MOD" "$WORK/badFont.res" > /dev/null 2> "$WORK/resBadFontBuild.txt"
    +expectRc "dvxres build with a non-font font fails" 1 $?
    +expectGrep "dvxres build with a non-font font message" "badFont.res:1: blob.bin is not a DVX font file of this version." "$WORK/resBadFontBuild.txt"
     
     # strip restores the original byte-for-byte; a second strip is a no-op.
     "$HOST/dvxres" strip "$MOD" > "$WORK/resStrip.txt" 2>&1
    @@ -825,7 +884,10 @@ done
     # 2c. dvxres -- injected allocator and I/O failures (resFailTest)
     # ------------------------------------------------------------------
     echo "-- dvxres failure injection"
    -if $HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -I"$TOOLS" -o "$WORK/resFailTest" "$HERE/resFailTest.c" "$LIBDVX/dvxResource.c" "$STB_DS_IMPL" > "$WORK/resFailBuild.log" 2>&1; then
    +# resFailTest.c includes dvxres.c, dvxResWrite.c, and the platform
    +# utilities (under its wrappers); the reader, the font reader dvxres
    +# checks font resources with, and stb_ds are linked as they are.
    +if $HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -I"$TOOLS" -o "$WORK/resFailTest" "$HERE/resFailTest.c" "$LIBDVX/dvxResource.c" "$LIBDVX/dvxFontFmt.c" "$STB_DS_IMPL" > "$WORK/resFailBuild.log" 2>&1; then
         pass "resFailTest builds"
     
         # A module with one resource; every failed add must leave it untouched.
    @@ -875,7 +937,7 @@ if $HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -I"$TOOLS" -o "$WORK/resFailTest"
         # 2d. dvxres with dvxResource.c compiled under the same wrappers: sweep
         # every injected call ordinal through an add; every failure must leave
         # the module untouched and no temp file behind.
    -    if $HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -I"$TOOLS" -DRES_WRAP_RESOURCE -o "$WORK/resReadFailTest" "$HERE/resFailTest.c" "$STB_DS_IMPL" > "$WORK/resReadFailBuild.log" 2>&1; then
    +    if $HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -I"$TOOLS" -DRES_WRAP_RESOURCE -o "$WORK/resReadFailTest" "$HERE/resFailTest.c" "$LIBDVX/dvxFontFmt.c" "$STB_DS_IMPL" > "$WORK/resReadFailBuild.log" 2>&1; then
             pass "resReadFailTest builds"
             for cmd in add list get strip rmlast; do
                 for mode in malloc realloc fopen fread fseek fwrite fclose rename; do
    @@ -1142,12 +1204,206 @@ expectNoGrep "mkicon write failure prints no summary" "Generated" "$WORK/iw4.out
     "$HOST/mkwgticon" "$WORK/x.bmp" > /dev/null 2> "$WORK/iu5.err"
     expectGrep "mkwgticon usage lists the types" "Types: button label" "$WORK/iu5.err"
     
    +# ------------------------------------------------------------------
    +# 3c. mkfont -- fonts, their notices and licences, preview, the .DFN reader
    +# ------------------------------------------------------------------
    +# The fixture manifest (fixtures/fonts/fonts.ini) converts two small BDF
    +# faces and DejaVu Sans Oblique (toolchains/fonts).  Its sources and the
    +# licence texts are gathered in one source folder here; the UTF-8 and
    +# broken licence texts are written here so the repository stays ASCII.
    +echo "-- mkfont"
    +FONT_FIX="$FIX/fonts"
    +FONT_SRC="$WORK/fontSrc"
    +mkdir -p "$FONT_SRC" "$WORK/dfnCorrupt"
    +cp "$FONT_FIX"/*.bdf "$FONT_SRC"/
    +for d in dejavu exo2 pacifico; do
    +    ln -s "$FONT_SOURCES/$d" "$FONT_SRC/$d"
    +done
    +printf '\xe2\x80\x9cTiny\xe2\x80\x9d licence \xe2\x80\x93 \xc2\xa9 2026 \xc3\x86thelred\r\nSecond line' > "$FONT_SRC/lic1.txt"
    +printf 'Plain ASCII licence.\n' > "$FONT_SRC/lic2.txt"
    +printf 'A snowman \xe2\x98\x83 here\n' > "$FONT_SRC/licSnowman.txt"
    +printf 'Broken \xc3\x28 UTF-8\n' > "$FONT_SRC/licBroken.txt"
    +awk '/^STARTCHAR space/ { skip = 1 } !skip { sub(/^CHARS 5$/, "CHARS 4"); print } /^ENDCHAR/ { skip = 0 }' "$FONT_FIX/tiny.bdf" > "$FONT_SRC/nospace.bdf"
    +
    +"$HOST/mkfont" "$FONT_FIX/fonts.ini" "$FONT_SRC" "$WORK/fonts1" > "$WORK/mkfont1.out" 2> "$WORK/mkfont1.err"
    +expectRc "mkfont fixture manifest" 0 $?
    +"$HOST/mkfont" "$FONT_FIX/fonts.ini" "$FONT_SRC" "$WORK/fonts2" > /dev/null 2>&1
    +check "mkfont is deterministic" diff -r "$WORK/fonts1" "$WORK/fonts2"
    +expectGrep "mkfont lists what TINY lacks at some size" "TINY      2 sizes,  0 smoothed, missing 253: 00-1F 21-40 42-C3 C5-FF" "$WORK/mkfont1.out"
    +expectGrep "mkfont lists what TINYF lacks" "TINYF     1 sizes,  0 smoothed, missing 252: 00 02-1F 21-40 42-C3 C5-FF" "$WORK/mkfont1.out"
    +expectGrep "mkfont lists what TINYS lacks" "TINYS     3 sizes,  1 smoothed, missing   1: 00" "$WORK/mkfont1.out"
    +for f in TINY TINYF TINYS; do
    +    golden "mkfont $f.DFN" "$WORK/fonts1/$f.DFN" "fonts/$f.DFN"
    +done
    +
    +# The reader (dvxFontFmt.c) under ASan/UBSan: the dumps show each
    +# glyph's record and ink (TINY: proportional, bold, box drawing by DVX,
    +# C cedilla drawn as nothing so missing; TINYF: fixed pitch)
    +if $HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/dfnReadTest" "$HERE/dfnReadTest.c" "$LIBDVX/dvxFontFmt.c" > "$WORK/dfnBuild.log" 2>&1; then
    +    pass "dfnReadTest builds"
    +    for f in TINY TINYF; do
    +        "$WORK/dfnReadTest" dump "$WORK/fonts1/$f.DFN" > "$WORK/$f.dump" 2>&1
    +        expectRc "dfnReadTest dump $f" 0 $?
    +        golden "dfnReadTest dump $f" "$WORK/$f.dump" "fonts/$f.dump"
    +    done
    +    expectGrep "TINY is bold with DVX box drawing, not fixed pitch" "family 'Tiny' style 1 flags 2 strikes 2" "$WORK/TINY.dump"
    +    expectGrep "TINYF is fixed pitch" "family 'Tiny Fixed' style 0 flags 1 strikes 1" "$WORK/TINYF.dump"
    +
    +    # The notice and licence texts each file carries (dump lines end in
    +    # "|", a CR LF line end shown as "\r|")
    +    expectGrep "mkfont notice names the face and its sources" "  Tiny Bold, converted for DVX from tiny437.bdf, tiny.bdf.\\r|" "$WORK/TINY.dump"
    +    expectGrep "mkfont notice keeps a BDF notice" "  Tiny test face, public domain\\r|" "$WORK/TINY.dump"
    +    expectGrep "mkfont transliterates a licence to ASCII" "\"Tiny\" licence - (C) 2026 AEthelred" "$WORK/TINY.dump"
    +    expectGrep "mkfont joins a licence's texts" "  Plain ASCII licence.\\r|" "$WORK/TINY.dump"
    +    check "mkfont texts have DOS line ends only" test "$(awk '/^(notice|licence) [0-9]+ bytes:/ { t = 1; next } /^strike/ { t = 0 } t && !/\\r\|$/' "$WORK/TINY.dump" | wc -l)" = 0
    +    check "mkfont texts are ASCII" python3 -c 'import sys; open(sys.argv[1], "rb").read().decode("ascii")' "$WORK/TINY.dump"
    +    expectNoGrep "a glyph drawn as nothing is missing" "  80:" "$WORK/TINY.dump"
    +    "$WORK/dfnReadTest" dump "$WORK/fonts1/TINYS.DFN" > "$WORK/TINYS.dump" 2>&1
    +    expectRc "dfnReadTest dump TINYS" 0 $?
    +    expectGrep "TINYS has a gray 12 px strike" "strike 12 px gray:" "$WORK/TINYS.dump"
    +    expectGrep "mkfont notice keeps an sfnt notice" "  Copyright (c) 2003 by Bitstream, Inc." "$WORK/TINYS.dump"
    +    check "a gray strike shows partial levels" grep -q "^    [.#]*[:+]" "$WORK/TINYS.dump"
    +    "$WORK/dfnReadTest" check "$WORK/fonts1"/*.DFN > "$WORK/dfnCheck.out" 2>&1
    +    expectRc "dfnReadTest check (fixtures)" 0 $?
    +    "$WORK/dfnReadTest" corrupt "$WORK/fonts1/TINY.DFN" "$WORK/dfnCorrupt" > "$WORK/dfnCorrupt.out" 2>&1
    +    expectRc "dfnReadTest refuses damaged files" 0 $?
    +    expectNoGrep "dfnReadTest corrupt has no failures" "FAIL" "$WORK/dfnCorrupt.out"
    +    "$WORK/dfnReadTest" sources "$WORK/fonts1/TINY.DFN" "$WORK/dfnCorrupt" > "$WORK/dfnSources.out" 2>&1
    +    expectRc "dfnReadTest reads a font inside a file and in memory" 0 $?
    +    expectGrep "dfnReadTest a font inside a file" "ok inside a file" "$WORK/dfnSources.out"
    +    expectGrep "dfnReadTest a font in memory" "ok in memory" "$WORK/dfnSources.out"
    +    "$WORK/dfnReadTest" > /dev/null 2> "$WORK/dfnUsage.err"
    +    expectRc "dfnReadTest usage" 2 $?
    +
    +    # Every font DVX ships converts, and the reader takes each one
    +    "$HOST/mkfont" "$ROOT/src/fonts/fonts.ini" "$FONT_SOURCES" "$WORK/fontsAll" > "$WORK/mkfontAll.out" 2> "$WORK/mkfontAll.err"
    +    expectRc "mkfont converts the shipped manifest" 0 $?
    +    "$WORK/dfnReadTest" check "$WORK/fontsAll"/*.DFN > "$WORK/dfnCheckAll.out" 2>&1
    +    expectRc "dfnReadTest check (every shipped font)" 0 $?
    +
    +    # graycut: a listed size's 1-bit glyphs are cut from the anti-aliased
    +    # rendering (DejaVu Serif Bold, which its 1-bit hinting draws near
    +    # regular weight at 20 px, comes out heavier); other sizes keep it
    +    printf '%s\n' "[fonts]" "SERFH = Serif" "SERFG = Serif" "[licences]" "LIC = lic2.txt" "[SERFH]" "style = bold" "licence = LIC" "source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif-Bold.ttf" "sizes = 16 20" "[SERFG]" "style = bold" "licence = LIC" "source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif-Bold.ttf" "sizes = 16 20" "graycut = 20" > "$WORK/grayCut.ini"
    +    "$HOST/mkfont" "$WORK/grayCut.ini" "$FONT_SRC" "$WORK/grayCut" > /dev/null 2>&1
    +    expectRc "mkfont gray-cut sizes" 0 $?
    +    strikeDump() {
    +        # strikeDump    -- that 1-bit strike of the font's dump
    +        "$WORK/dfnReadTest" dump "$WORK/grayCut/$1.DFN" | awk -v want="strike $2 px:" '/^strike/ { on = index($0, want) == 1 } on'
    +    }
    +    check "a size not gray-cut keeps its hinting" cmp -s <(strikeDump SERFH 16) <(strikeDump SERFG 16)
    +    check "a gray-cut size is heavier" test "$(strikeDump SERFG 20 | grep '^    ' | tr -cd '#' | wc -c)" -gt "$(strikeDump SERFH 20 | grep '^    ' | tr -cd '#' | wc -c)"
    +
    +    # chars: only the characters listed are made, the rest are missing
    +    printf '%s\n' "[fonts]" "PART = Part" "[licences]" "LIC = lic2.txt" "[PART]" "licence = LIC" "source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans.ttf" "sizes = 12" "chars = 20 41-43, 7E" > "$WORK/chars.ini"
    +    "$HOST/mkfont" "$WORK/chars.ini" "$FONT_SRC" "$WORK/chars" > "$WORK/chars.out" 2>&1
    +    expectRc "mkfont chars" 0 $?
    +    "$WORK/dfnReadTest" dump "$WORK/chars/PART.DFN" > "$WORK/chars.dump" 2>&1
    +    check "mkfont chars makes the characters listed" test "$(grep -c '^  \(20\|41\|42\|43\|7E\):' "$WORK/chars.dump")" = 5
    +    check "mkfont chars makes no others" test "$(grep -c '^  [0-9A-F][0-9A-F]:' "$WORK/chars.dump")" = 5
    +else
    +    fail "dfnReadTest build"
    +    cat "$WORK/dfnBuild.log"
    +fi
    +
    +# The preview sheet (24-bit, too big for a golden): the same on every
    +# run; the sample line starts with "8", which TINY lacks (a red box at
    +# the top-left margin), and the grid's 'A' (row 4, column 1) has its
    +# apex ink in its first row (column 2)
    +"$HOST/mkfont" -p "$WORK/fonts1/TINY.DFN" "$WORK/tiny.bmp" > /dev/null 2>&1
    +expectRc "mkfont preview" 0 $?
    +"$HOST/mkfont" -p "$WORK/fonts1/TINY.DFN" "$WORK/tiny2.bmp" > /dev/null 2>&1
    +check "mkfont preview is deterministic" cmp -s "$WORK/tiny.bmp" "$WORK/tiny2.bmp"
    +bmpPixel() {
    +    # bmpPixel     -- "r,g,b" of a 24-bit bottom-up BMP
    +    python3 - "$@" <<'PYEOF'
    +import struct, sys
    +data = open(sys.argv[1], "rb").read()
    +off, = struct.unpack_from(" /dev/null 2> "$WORK/mkfontP.err"
    +expectRc "mkfont preview of a non-font fails" 1 $?
    +expectGrep "mkfont preview of a non-font message" "is not a DVX font file" "$WORK/mkfontP.err"
    +"$HOST/mkfont" > /dev/null 2> "$WORK/mkfontU.err"
    +expectRc "mkfont without arguments fails" 2 $?
    +expectGrep "mkfont usage" "usage: mkfont MANIFEST SOURCEDIR OUTDIR" "$WORK/mkfontU.err"
    +"$HOST/mkfont" "$FONT_FIX/fonts.ini" "$FONT_SRC" "$FONT_FIX/fonts.ini/out" > /dev/null 2> "$WORK/mkfontD.err"
    +expectRc "mkfont unwritable output folder fails" 1 $?
    +expectGrep "mkfont unwritable output folder message" "cannot make" "$WORK/mkfontD.err"
    +
    +# Manifest errors: each stops the run naming what is wrong
    +mkfontFails() {
    +    # mkfontFails   
    +    local name="$1"
    +    local want="$2"
    +    shift 2
    +    printf '%s\n' "$@" > "$WORK/badFont.ini"
    +    rm -rf "$WORK/badFont"
    +    "$HOST/mkfont" "$WORK/badFont.ini" "$FONT_SRC" "$WORK/badFont" > /dev/null 2> "$WORK/badFont.err"
    +    expectRc "mkfont $name fails" 1 $?
    +    expectGrep "mkfont $name message" "$want" "$WORK/badFont.err"
    +}
    +LICS=("[licences]" "LIC = lic2.txt")
    +mkfontFails "no font list" "has no [fonts] list" "${LICS[@]}"
    +mkfontFails "lower-case name" "font name 'tiny' is not 1 to 8 capitals" "[fonts]" "tiny = Tiny" "${LICS[@]}"
    +mkfontFails "long name" "font name 'TOOLONGNM' is not 1 to 8 capitals" "[fonts]" "TOOLONGNM = Tiny" "${LICS[@]}"
    +mkfontFails "long family" "its family name is missing or longer than 31 characters" "[fonts]" "TINY = Family name of thirty-two chars!" "${LICS[@]}"
    +mkfontFails "unknown licence" "licence 'NOPE' is not in [licences]" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = NOPE" "source = tiny.bdf"
    +mkfontFails "missing licence text" "cannot read" "[fonts]" "TINY = Tiny" "[licences]" "LIC = nosuch.txt"
    +mkfontFails "licence character with no ASCII form" "no ASCII form for U+2603" "[fonts]" "TINY = Tiny" "[licences]" "LIC = licSnowman.txt"
    +mkfontFails "licence that is not UTF-8" "licBroken.txt:1: is not UTF-8" "[fonts]" "TINY = Tiny" "[licences]" "LIC = licBroken.txt"
    +mkfontFails "unknown style" "style 'heavy' is not one of: regular bold italic bolditalic" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "style = heavy" "source = tiny.bdf"
    +mkfontFails "unknown hinting" "hinting 'fast' is not one of: native auto none" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = tiny.bdf" "hinting = fast"
    +mkfontFails "unknown encoding" "encoding1 'ebcdic' is not one of: unicode cp437" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = tiny.bdf" "source1 = tiny437.bdf" "encoding1 = ebcdic"
    +mkfontFails "bad size" "size '12x' is not a pixel size" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans.ttf" "sizes = 10 12x"
    +mkfontFails "size too large" "size '128' is not a pixel size" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans.ttf" "sizes = 128"
    +mkfontFails "too many sizes" "or there are over 32" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans.ttf" "sizes = $(seq -s ' ' 1 33)"
    +mkfontFails "empty size list" "TINY: no sizes" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans.ttf" "sizes = ,"
    +mkfontFails "no sources" "TINY: 0 sizes (1 to 32 allowed)" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC"
    +mkfontFails "missing source" "cannot open" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = nosuch.bdf"
    +mkfontFails "native sizes of an outline face" "is not a bitmap face" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans.ttf"
    +mkfontFails "sizes of a bitmap face" "is a bitmap face (give sizes = native)" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = tiny.bdf" "sizes = 8"
    +mkfontFails "weight of a fixed face" "is not a variable face" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans.ttf" "sizes = 10" "weight = 700"
    +mkfontFails "two sources of one size" "two of its sources make 8 px" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = tiny.bdf" "source1 = tiny.bdf"
    +mkfontFails "a face without a space" "has no space at 8 px" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = nospace.bdf"
    +mkfontFails "a code page 437 face read as Unicode" "has no Unicode character map" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = tiny437.bdf"
    +mkfontFails "smoothed sizes of a bitmap face" "is a bitmap face: it has no smoothed sizes" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = tiny.bdf" "smooth = 8"
    +mkfontFails "chars not hex" "chars 'G0' is not a hex code or range from 00 to FF" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = tiny.bdf" "chars = 20 G0"
    +mkfontFails "chars range backwards" "chars '7E-20' is not a hex code or range" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = tiny.bdf" "chars = 7E-20"
    +mkfontFails "chars past FF" "chars '20-100' is not a hex code or range" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = tiny.bdf" "chars = 20-100"
    +mkfontFails "chars range without its end" "chars '20-' is not a hex code or range" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = tiny.bdf" "chars = 20-"
    +mkfontFails "a smoothed size it does not make" "smoothed size 13 is not among its sizes" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans.ttf" "sizes = 12" "smooth = 13"
    +mkfontFails "gray-cut sizes of a bitmap face" "is a bitmap face: it has no gray-cut sizes" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = tiny.bdf" "graycut = 8"
    +mkfontFails "a gray-cut size it does not make" "gray-cut size 13 is not among its sizes" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans.ttf" "sizes = 12" "graycut = 13"
    +mkfontFails "a glyph too large to store" "has a glyph too large for a FontGlyphT" "[fonts]" "TINY = Tiny" "${LICS[@]}" "[TINY]" "licence = LIC" "source = pacifico/Pacifico-Regular.ttf" "sizes = 127"
    +
    +# mkfont -l: the files of a set, in manifest order
    +printf '%s\n' "[fonts]" "ONE = One" "TWO = Two" "THREE = Three" "[ONE]" "set = base" "[THREE]" "set = BASE" > "$WORK/set.ini"
    +"$HOST/mkfont" -l "$WORK/set.ini" base > "$WORK/set.out" 2>&1
    +expectRc "mkfont -l" 0 $?
    +check "mkfont -l lists the set's files only" test "$(tr '\n' ' ' < "$WORK/set.out")" = "ONE.DFN THREE.DFN "
    +
    +# A variable face instanced at two weights makes two different files
    +printf '%s\n' "[fonts]" "EXOR = Exo" "EXOB = Exo" "[licences]" "LIC = lic2.txt" "[EXOR]" "licence = LIC" "source = exo2/Exo2[wght].ttf" "sizes = 16" "weight = 400" "[EXOB]" "style = bold" "licence = LIC" "source = exo2/Exo2[wght].ttf" "sizes = 16" "weight = 700" > "$WORK/exo.ini"
    +"$HOST/mkfont" "$WORK/exo.ini" "$FONT_SRC" "$WORK/exo" > /dev/null 2>&1
    +expectRc "mkfont instances a variable face" 0 $?
    +check "mkfont weights differ" test "$(cmp -s "$WORK/exo/EXOR.DFN" "$WORK/exo/EXOB.DFN"; echo $?)" = "1"
    +
     # ------------------------------------------------------------------
     # 4. hlpcCompile library mode (HLPC_NO_MAIN) under ASan/UBSan
     # ------------------------------------------------------------------
     echo "-- hlpcCompile library mode"
     cd "$HERE" || exit 1
    -if $HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -DHLPC_NO_MAIN -o "$WORK/hlpcLibTest" hlpcLibTest.c "$TOOLS/dvxhlpc.c" "$PLATFORM_UTIL" "$STB_DS_IMPL" > "$WORK/libBuild.log" 2>&1; then
    +if $HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -DHLPC_NO_MAIN -o "$WORK/hlpcLibTest" hlpcLibTest.c "${dvxhlpcSrcs[@]}" > "$WORK/libBuild.log" 2>&1; then
         pass "hlpcLibTest builds with -DHLPC_NO_MAIN under ASan/UBSan"
         "$WORK/hlpcLibTest" "$WORK/lib.hlp" "$WORK/lib.html" "$WORK/libNeg.hlp" "$WORK/libLenient.hlp" "$WORK/libLenient.html" > "$WORK/libRun.out" 2> "$WORK/libRun.err"
         expectRc "hlpcLibTest run" 0 $?
    @@ -1191,16 +1447,18 @@ fi
     # ------------------------------------------------------------------
     echo "-- sanitizer builds of the CLI tools"
     sanOk=1
    -$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/dvxhlpcSan" "$TOOLS/dvxhlpc.c" "$PLATFORM_UTIL" "$STB_DS_IMPL" > "$WORK/sanBuild.log" 2>&1 || sanOk=0
    -$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/dvxresSan" "$TOOLS/dvxres.c" "$TOOLS/dvxResWrite.c" "$LIBDVX/dvxResource.c" "$PLATFORM_UTIL" "$STB_DS_IMPL" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0
    -$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/mkiconSan" "$TOOLS/mkicon.c" "$TOOLS/bmpDraw.c" -lm >> "$WORK/sanBuild.log" 2>&1 || sanOk=0
    -$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/mktbiconSan" "$TOOLS/mktbicon.c" "$TOOLS/bmpDraw.c" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0
    -$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/mkwgticonSan" "$TOOLS/mkwgticon.c" "$TOOLS/bmpDraw.c" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0
    -$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/bmp2rawSan" "$TOOLS/bmp2raw.c" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0
    -$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/mksoundsSan" "$TOOLS/mkSounds.c" -lm >> "$WORK/sanBuild.log" 2>&1 || sanOk=0
    +$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/dvxhlpcSan" "${dvxhlpcSrcs[@]}" > "$WORK/sanBuild.log" 2>&1 || sanOk=0
    +$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/dvxresSan" "${dvxresSrcs[@]}" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0
    +$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/mkiconSan" "${mkiconSrcs[@]}" -lm >> "$WORK/sanBuild.log" 2>&1 || sanOk=0
    +$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/mktbiconSan" "${mktbiconSrcs[@]}" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0
    +$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/mkwgticonSan" "${mkwgticonSrcs[@]}" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0
    +$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/bmp2rawSan" "${bmp2rawSrcs[@]}" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0
    +$HOSTCC "${SAN_FLAGS[@]}" "${INCLUDES[@]}" -o "$WORK/mksoundsSan" "${mksoundsSrcs[@]}" -lm >> "$WORK/sanBuild.log" 2>&1 || sanOk=0
    +# dvxPrefs.c builds with the project's warnings (common.mk WARNFLAGS)
    +$HOSTCC "${SAN_FLAGS[@]}" -Wno-sign-compare -Wno-type-limits "${INCLUDES[@]}" -I"$FREETYPE/include/freetype2" -o "$WORK/mkfontSan" "${mkfontSrcs[@]}" "$FREETYPE/lib/libfreetype.a" >> "$WORK/sanBuild.log" 2>&1 || sanOk=0
     
     if [ "$sanOk" = "1" ]; then
    -    pass "sanitizer builds of all six tools"
    +    pass "sanitizer builds of every tool"
         : > "$WORK/san.err"
         cd "$FIX/help" || exit 1
         "$WORK/dvxhlpcSan" -o "$WORK/san.hlp" --html "$WORK/san.html" --quiet -i . main.dhs @inputs.txt >> "$WORK/san.err" 2>&1
    @@ -1243,6 +1501,10 @@ if [ "$sanOk" = "1" ]; then
         check "bmp2raw (ASan) matches" cmp -s "$WORK/san.raw" "$WORK/splash.raw"
         "$WORK/mksoundsSan" startup "$WORK/sanStartup.wav" >> "$WORK/san.err" 2>&1
         check "mksounds (ASan) matches" cmp -s "$WORK/sanStartup.wav" "$WORK/snd_startup.wav"
    +    "$WORK/mkfontSan" "$FONT_FIX/fonts.ini" "$FONT_SRC" "$WORK/sanFonts" >> "$WORK/san.err" 2>&1
    +    check "mkfont (ASan) matches" diff -r "$WORK/sanFonts" "$WORK/fonts1"
    +    "$WORK/mkfontSan" -p "$WORK/fonts1/TINYS.DFN" "$WORK/sanTinys.bmp" >> "$WORK/san.err" 2>&1
    +    expectRc "mkfont (ASan) preview" 0 $?
         expectNoGrep "no sanitizer reports from CLI tools" "runtime error" "$WORK/san.err"
         expectNoGrep "no ASan reports from CLI tools" "AddressSanitizer" "$WORK/san.err"
         expectNoGrep "no leak reports from CLI tools" "LeakSanitizer" "$WORK/san.err"
    diff --git a/src/test/unit/Makefile b/src/test/unit/Makefile
    index d4c5f0b1..22dd57cf 100644
    --- a/src/test/unit/Makefile
    +++ b/src/test/unit/Makefile
    @@ -34,6 +34,7 @@ OBJDIR    = $(HOST_OBJROOT)
     BINDIR    = $(OBJDIR)/test/unit
     LIBDVX    = $(ROOT)/src/libs/kpunch/libdvx
     LIBTASKS  = $(ROOT)/src/libs/kpunch/libtasks
    +RICHTEXT  = $(ROOT)/src/libs/kpunch/richtext
     WIDGETS   = $(ROOT)/src/widgets/kpunch
     WIDGETDIR = $(OBJDIR)/widgets
     SCRATCH   = $(BINDIR)/scratch
    @@ -43,8 +44,12 @@ CFLAGS    = $(HOST_CFLAGS) -I$(LIBDVX) -I$(LIBDVX)/platform -I$(LIBDVX)/thirdpar
     LDFLAGS   = -rdynamic $(HOST_LDFLAGS)
     LDLIBS    = -ldl -lm
     
    -SRCS      = unitMain.c unitDisplay.c testClass.c testClock.c testComp.c testDraw.c testEvent.c testIface.c testLayout.c testMem.c testPlatform.c testPrefs.c testResource.c testUtil.c testVideo.c testWm.c
    +SRCS      = unitMain.c unitDisplay.c testClass.c testClock.c testComp.c testDraw.c testEvent.c testFlow.c testFonts.c testIface.c testLayout.c testMem.c testPlatform.c testPrefs.c testResource.c testRichText.c testUtil.c testVideo.c testWm.c
     OBJS      = $(patsubst %.c,$(BINDIR)/%.o,$(SRCS))
    +
    +# The styled text library is not in libdvx: its sources build in here
    +LIB_SRCS  = $(wildcard $(RICHTEXT)/*.c)
    +LIB_OBJS  = $(patsubst %.c,$(BINDIR)/lib/%.o,$(notdir $(LIB_SRCS)))
     TARGET    = $(BINDIR)/unit
     
     # stb_ds allocation-failure test: its own binary, since it compiles its own
    @@ -69,8 +74,12 @@ $(BINDIR)/%.o: %.c $(DVX_MAKEFILES)
     	@mkdir -p $(BINDIR)
     	$(HOSTCC) $(CFLAGS) -c -o $@ $<
     
    -$(TARGET): $(OBJS) $(LIBDVX_A)
    -	$(HOSTCC) $(LDFLAGS) -o $@ $(OBJS) -Wl,--whole-archive $(LIBDVX_A) -Wl,--no-whole-archive $(LDLIBS)
    +$(BINDIR)/lib/%.o: $(RICHTEXT)/%.c $(DVX_MAKEFILES)
    +	@mkdir -p $(BINDIR)/lib
    +	$(HOSTCC) $(CFLAGS) -c -o $@ $<
    +
    +$(TARGET): $(OBJS) $(LIB_OBJS) $(LIBDVX_A)
    +	$(HOSTCC) $(LDFLAGS) -o $@ $(OBJS) $(LIB_OBJS) -Wl,--whole-archive $(LIBDVX_A) -Wl,--no-whole-archive $(LDLIBS)
     
     $(BINDIR)/oom/%.o: %.c $(DVX_MAKEFILES)
     	@mkdir -p $(BINDIR)/oom
    @@ -88,7 +97,7 @@ run: $(TARGET) $(OOM_TARGET) | widgets
     	ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 $(TARGET) $(abspath $(WIDGETDIR)) $(abspath $(SCRATCH))
     	ASAN_OPTIONS=detect_leaks=1 $(OOM_TARGET) $(abspath $(SCRATCH))
     
    --include $(OBJS:.o=.d) $(OOM_OBJS:.o=.d)
    +-include $(OBJS:.o=.d) $(LIB_OBJS:.o=.d) $(OOM_OBJS:.o=.d)
     
     clean:
     	rm -rf $(BINDIR)
    diff --git a/src/test/unit/testDraw.c b/src/test/unit/testDraw.c
    index 3a050566..271b6287 100644
    --- a/src/test/unit/testDraw.c
    +++ b/src/test/unit/testDraw.c
    @@ -78,6 +78,20 @@
     #define TERM_FONTS        2
     #define TERM_SMALL_GLYPHS 2      // 'B' and 'C' only
     
    +// The hand-built proportional font (propFontInit): 11-pixel lines with
    +// the baseline 8 rows down; glyphs of 1 and 2 bytes per row, one left of
    +// the pen, one reaching past its advance, one below the baseline
    +#define PROP_ASCENT      8
    +#define PROP_HEIGHT      11
    +#define PROP_DIGIT_W     5
    +#define PROP_BLANK_ADV   5
    +#define PROP_UNDERLINE_Y 9
    +#define PROP_STRIKE_Y    5
    +#define PROP_BITS_MAX    512
    +#define PROP_PATTERN     4       // a glyph pixel is set unless (3x + 5y + ch) is a multiple of this
    +#define PROP_X_STEP      3
    +#define PROP_Y_STEP      5
    +
     #define TRANS_KEY       0x0F0F0F0Fu
     #define TRANS_SRC_W     4
     #define TRANS_SRC_H     2
    @@ -86,6 +100,33 @@
     
     static const int32_t sBpps[] = { 8, 16, 32 };
     
    +// The glyphs of the proportional font: character, advance, bitmap size
    +// and offset from the pen (every other character is a blank PROP_BLANK_ADV
    +// wide)
    +typedef struct {
    +    uint8_t ch;
    +    uint8_t advance;
    +    uint8_t width;
    +    uint8_t height;
    +    int8_t  left;
    +    int8_t  top;
    +} PropGlyphDefT;
    +
    +static const PropGlyphDefT sPropDefs[] = {
    +    { ' ', 3,  0,  0,  0, 0 },
    +    { '0', 5,  4,  7,  0, 7 },
    +    { 'A', 7,  6,  7,  0, 7 },
    +    { 'W', 12, 11, 7,  0, 7 },
    +    { 'g', 6,  13, 10, -2, 7 },
    +    { 'i', 2,  5,  7,  0, 7 },
    +    { 'j', 4,  3,  9, -1, 7 }
    +};
    +
    +static FontGlyphT  sPropGlyphs[FONT_NUM_CHARS];
    +static uint32_t    sPropAt[FONT_NUM_CHARS];
    +static uint8_t     sPropBits[PROP_BITS_MAX];
    +static BitmapFontT sPropFont;
    +
     
     // ============================================================
     // Prototypes
    @@ -95,7 +136,11 @@ static void     bufPixelSet(uint8_t *buf, int32_t pitch, int32_t bytes, int32_t
     static void     checkGuards(const uint8_t *buf, int32_t total, int32_t used, int32_t line);
     static uint32_t colorForBpp(int32_t bpp, uint32_t c);
     static uint32_t expectedGray(const DisplayT *d, uint32_t px);
    +static int32_t  firstMismatch(const DisplayT *d, const uint32_t *ref);
    +static void     primeSurface(DisplayT *d, uint32_t *ref, uint32_t value);
    +static void     propFontInit(void);
     static void     referenceText(DisplayT *d, int32_t x, int32_t y, const char *text, int32_t scale, uint32_t fg, uint32_t bg, bool opaque, uint32_t *out);
    +static void     referenceTextProp(const DisplayT *d, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t scale, uint32_t fg, uint32_t bg, bool opaque, uint32_t *out);
     static void     testArrowGlyph(void);
     static void     testBevel(int32_t bpp);
     static void     testBevelWide(void);
    @@ -110,8 +155,11 @@ static void     testScrollbarArrowButton(void);
     static void     testSpans(void);
     static void     testTermRow(int32_t bpp);
     static void     testText(int32_t bpp);
    +static void     testTextProp(int32_t bpp);
     static void     testTextScaled(int32_t bpp);
    +static void     testTextStyled(int32_t bpp);
     static void     testTextWidth(void);
    +static void     testTextWidthProp(void);
     static void     testTransparent(int32_t bpp);
     
     
    @@ -181,6 +229,82 @@ static uint32_t expectedGray(const DisplayT *d, uint32_t px) {
     }
     
     
    +// The first pixel (index) where the surface differs from ref, or -1
    +static int32_t firstMismatch(const DisplayT *d, const uint32_t *ref) {
    +    for (int32_t i = 0; i < d->width * d->height; i++) {
    +        if (unitPixelGet(d, i % d->width, i / d->width) != ref[i]) {
    +            return i;
    +        }
    +    }
    +
    +    return -1;
    +}
    +
    +
    +// Fill the surface and its reference with value
    +static void primeSurface(DisplayT *d, uint32_t *ref, uint32_t value) {
    +    for (int32_t y = 0; y < d->height; y++) {
    +        for (int32_t x = 0; x < d->width; x++) {
    +            unitPixelSet(d, x, y, value);
    +            ref[y * d->width + x] = value;
    +        }
    +    }
    +}
    +
    +
    +// Build the proportional test font (sPropDefs) as dvxFontOpen would
    +// hand one out: glyph records, bitmaps back to back, zero bits past each
    +// glyph's width
    +static void propFontInit(void) {
    +    uint32_t at = 0;
    +
    +    memset(sPropGlyphs, 0, sizeof(sPropGlyphs));
    +    memset(sPropBits, 0, sizeof(sPropBits));
    +
    +    for (int32_t c = 0; c < FONT_NUM_CHARS; c++) {
    +        sPropGlyphs[c].advance = PROP_BLANK_ADV;
    +        sPropAt[c]             = 0;
    +    }
    +
    +    for (int32_t i = 0; i < DVX_ARRAY_LEN(sPropDefs); i++) {
    +        const PropGlyphDefT *def      = &sPropDefs[i];
    +        FontGlyphT          *g        = &sPropGlyphs[def->ch];
    +        int32_t              rowBytes = FONT_ROW_BYTES(def->width);
    +
    +        g->advance      = def->advance;
    +        g->width        = def->width;
    +        g->height       = def->height;
    +        g->left         = def->left;
    +        g->top          = def->top;
    +        sPropAt[def->ch] = at;
    +
    +        for (int32_t y = 0; y < def->height; y++) {
    +            for (int32_t x = 0; x < def->width; x++) {
    +                if ((x * PROP_X_STEP + y * PROP_Y_STEP + def->ch) % PROP_PATTERN != 0) {
    +                    sPropBits[at + y * rowBytes + FONT_PIXEL_BYTE(x)] |= FONT_PIXEL_BIT(x);
    +                }
    +            }
    +        }
    +
    +        at += FONT_GLYPH_BYTES(g);
    +    }
    +
    +    memset(&sPropFont, 0, sizeof(sPropFont));
    +    sPropFont.charWidth  = PROP_DIGIT_W;
    +    sPropFont.charHeight = PROP_HEIGHT;
    +    sPropFont.numChars   = FONT_NUM_CHARS;
    +    sPropFont.glyphData  = sPropBits;
    +    sPropFont.glyphs     = sPropGlyphs;
    +    sPropFont.glyphAt    = sPropAt;
    +    sPropFont.ascent     = PROP_ASCENT;
    +    sPropFont.pixelSize  = PROP_HEIGHT;
    +    sPropFont.flags      = FONT_FLAG_PROPORTIONAL;
    +    sPropFont.underlineY = PROP_UNDERLINE_Y;
    +    sPropFont.strikeY    = PROP_STRIKE_Y;
    +    sPropFont.lineThick  = 1;
    +}
    +
    +
     // Byte-exact reference rasteriser for drawText: renders into out[] (one
     // uint32 per pixel, pre-filled with the current backBuf contents) honouring
     // the display clip rect exactly as the API contract describes.
    @@ -213,6 +337,52 @@ static void referenceText(DisplayT *d, int32_t x, int32_t y, const char *text, i
     }
     
     
    +// Reference rasteriser for a loaded (proportional) font: an opaque run's
    +// background across its advances and line, then every glyph pixel at the
    +// pen, each dot a scale x scale block, clipped to the display clip rect
    +static void referenceTextProp(const DisplayT *d, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t scale, uint32_t fg, uint32_t bg, bool opaque, uint32_t *out) {
    +    int32_t width = 0;
    +
    +    for (int32_t i = 0; text[i]; i++) {
    +        width += font->glyphs[(uint8_t)text[i]].advance * scale;
    +    }
    +
    +    for (int32_t py = y; opaque && py < y + font->charHeight * scale; py++) {
    +        for (int32_t px = x; px < x + width; px++) {
    +            if (px >= d->clipX && px < d->clipX + d->clipW && py >= d->clipY && py < d->clipY + d->clipH) {
    +                out[py * d->width + px] = bg;
    +            }
    +        }
    +    }
    +
    +    int32_t pen = x;
    +
    +    for (int32_t i = 0; text[i]; i++) {
    +        const FontGlyphT *g    = &font->glyphs[(uint8_t)text[i]];
    +        const uint8_t    *bits = font->glyphData + font->glyphAt[(uint8_t)text[i]];
    +
    +        for (int32_t row = 0; row < g->height * scale; row++) {
    +            for (int32_t col = 0; col < g->width * scale; col++) {
    +                int32_t px = pen + g->left * scale + col;
    +                int32_t py = y + (font->ascent - g->top) * scale + row;
    +                int32_t gx = col / scale;
    +                int32_t gy = row / scale;
    +
    +                if (px < d->clipX || px >= d->clipX + d->clipW || py < d->clipY || py >= d->clipY + d->clipH) {
    +                    continue;
    +                }
    +
    +                if (bits[gy * FONT_ROW_BYTES(g->width) + FONT_PIXEL_BYTE(gx)] & FONT_PIXEL_BIT(gx)) {
    +                    out[py * d->width + px] = fg;
    +                }
    +            }
    +        }
    +
    +        pen += g->advance * scale;
    +    }
    +}
    +
    +
     // drawArrowGlyph: rows rows widening by 2 from the tip, rows / 2 from the
     // center, in the given colour; the scrollbar arrow is the 4-row case.
     static void testArrowGlyph(void) {
    @@ -350,15 +520,19 @@ static void testCharFallback(void) {
     
     
     void testDraw(void) {
    +    propFontInit();
         testSpans();
         testTextWidth();
    +    testTextWidthProp();
     
         for (int32_t i = 0; i < DVX_ARRAY_LEN(sBpps); i++) {
             testRectFill(sBpps[i]);
             testRectCopy(sBpps[i]);
             testBevel(sBpps[i]);
             testText(sBpps[i]);
    +        testTextProp(sBpps[i]);
             testTextScaled(sBpps[i]);
    +        testTextStyled(sBpps[i]);
             testTermRow(sBpps[i]);
             testGrayscale(sBpps[i]);
             testTransparent(sBpps[i]);
    @@ -937,6 +1111,54 @@ static void testTermRow(int32_t bpp) {
             resetClipRect(&d);
         }
     
    +    // A loaded font: each cell its pitch wide, the background filled, the
    +    // glyph's ink at the cell's pen (glyphs may reach past the cell)
    +    uint32_t *ref = (uint32_t *)calloc((size_t)UNIT_SCREEN_W * UNIT_SCREEN_H, sizeof(uint32_t));
    +
    +    for (int32_t ci = 0; ci < caseCount; ci++) {
    +        const TermCaseT *c = &cases[ci];
    +
    +        primeSurface(&d, ref, pre);
    +        setClipRect(&d, c->clipX, c->clipY, c->clipW, c->clipH);
    +
    +        for (int32_t col = 0; col < TERM_COLS; col++) {
    +            uint8_t  attr = line[col * 2 + 1];
    +            uint32_t fg   = palette[attr & TERM_ATTR_FG_MASK];
    +            uint32_t bg   = palette[(attr >> TERM_ATTR_BG_SHIFT) & TERM_ATTR_BG_MASK];
    +            int32_t  cx   = c->x + col * PROP_DIGIT_W;
    +            char     one[2];
    +
    +            if ((attr & TERM_ATTR_BLINK) && !c->blinkVisible) {
    +                fg = bg;
    +            }
    +
    +            if (col == c->cursorCol) {
    +                uint32_t tmp = fg;
    +
    +                fg = bg;
    +                bg = tmp;
    +            }
    +
    +            one[0] = (char)line[col * 2];
    +            one[1] = 0;
    +
    +            for (int32_t py = c->y; py < c->y + PROP_HEIGHT; py++) {
    +                for (int32_t px = cx; px < cx + PROP_DIGIT_W; px++) {
    +                    if (px >= d.clipX && px < d.clipX + d.clipW && py >= d.clipY && py < d.clipY + d.clipH) {
    +                        ref[py * d.width + px] = bg;
    +                    }
    +                }
    +            }
    +
    +            referenceTextProp(&d, &sPropFont, cx, c->y, one, 1, fg, bg, false, ref);
    +        }
    +
    +        drawTermRow(&d, &ops, &sPropFont, c->x, c->y, TERM_COLS, line, palette, c->blinkVisible, c->cursorCol);
    +        TEST_EQ(firstMismatch(&d, ref), -1);
    +        resetClipRect(&d);
    +    }
    +
    +    free(ref);
         unitDisplayDestroy(&d);
     }
     
    @@ -1075,6 +1297,124 @@ static void testText(int32_t bpp) {
     }
     
     
    +// Text in a loaded (proportional) font against the reference: glyphs of
    +// one and two bytes per row, left of the pen, past the advance and below
    +// the baseline; opaque and transparent; clipped on every side, a clip
    +// edge inside a glyph's byte (the per-pixel path) and outside it (the
    +// unrolled path).  drawChar and drawTextN (also drawChar per character
    +// at the advances) must draw the same.
    +static void testTextProp(int32_t bpp) {
    +    DisplayT  d;
    +    BlitOpsT  ops;
    +    uint32_t *ref = (uint32_t *)calloc((size_t)UNIT_SCREEN_W * UNIT_SCREEN_H, sizeof(uint32_t));
    +    uint32_t  fg  = colorForBpp(bpp, DRAW_FG);
    +    uint32_t  bg  = colorForBpp(bpp, DRAW_BG);
    +    uint32_t  pre = colorForBpp(bpp, DRAW_FILL);
    +
    +    unitDisplayCreate(&d, UNIT_SCREEN_W, UNIT_SCREEN_H, bpp);
    +    drawInit(&ops, &d);
    +
    +    typedef struct {
    +        int32_t     x;
    +        int32_t     y;
    +        const char *text;
    +        bool        opaque;
    +        int32_t     clipX;
    +        int32_t     clipY;
    +        int32_t     clipW;
    +        int32_t     clipH;
    +    } PropCaseT;
    +
    +    const PropCaseT cases[] = {
    +        {   4,   4, "AWgij 0", true,  0,  0,  UNIT_SCREEN_W, UNIT_SCREEN_H },
    +        {   4,   4, "AWgij 0", false, 0,  0,  UNIT_SCREEN_W, UNIT_SCREEN_H },
    +        {  -7,   3, "WgA",     true,  0,  0,  UNIT_SCREEN_W, UNIT_SCREEN_H },
    +        {   1,   3, "gW",      false, 0,  0,  UNIT_SCREEN_W, UNIT_SCREEN_H },
    +        {   3,  -5, "Wjg",     true,  0,  0,  UNIT_SCREEN_W, UNIT_SCREEN_H },
    +        {  50,  40, "WWg",     false, 0,  0,  UNIT_SCREEN_W, UNIT_SCREEN_H },
    +        {   2,   2, "WgWgW",   true,  7,  4,  13,            6             },
    +        {   2,   2, "WgWgW",   false, 9,  3,  20,            9             },
    +        {   0,   0, "AW",      true,  0,  0,  0,             0             },
    +        { 100, 100, "A",       true,  0,  0,  UNIT_SCREEN_W, UNIT_SCREEN_H },
    +        {   2,   2, "\x01\xFE", true,  0,  0,  UNIT_SCREEN_W, UNIT_SCREEN_H }
    +    };
    +
    +    for (int32_t ci = 0; ci < DVX_ARRAY_LEN(cases); ci++) {
    +        const PropCaseT *c = &cases[ci];
    +        int32_t          bad;
    +
    +        primeSurface(&d, ref, pre);
    +        setClipRect(&d, c->clipX, c->clipY, c->clipW, c->clipH);
    +        referenceTextProp(&d, &sPropFont, c->x, c->y, c->text, 1, fg, bg, c->opaque, ref);
    +        drawText(&d, &ops, &sPropFont, c->x, c->y, c->text, fg, bg, c->opaque);
    +        bad = firstMismatch(&d, ref);
    +
    +        if (bad < 0) {
    +            unitPass();
    +        } else {
    +            unitFail(__FILE__, __LINE__, "proportional drawText case %" PRId32 " (bpp %" PRId32 ") mismatch at pixel %" PRId32, ci, bpp, bad);
    +        }
    +
    +        // Character by character at the advances: the same ink.  (Opaque
    +        // cells drawn one by one cover a neighbor's overhang, so only the
    +        // transparent runs are compared.)
    +        int32_t pen = c->x;
    +
    +        primeSurface(&d, ref, pre);
    +        referenceTextProp(&d, &sPropFont, c->x, c->y, c->text, 1, fg, bg, false, ref);
    +
    +        for (int32_t i = 0; c->text[i]; i++) {
    +            pen += drawChar(&d, &ops, &sPropFont, pen, c->y, c->text[i], fg, bg, false);
    +        }
    +
    +        bad = firstMismatch(&d, ref);
    +
    +        if (bad < 0) {
    +            unitPass();
    +        } else {
    +            unitFail(__FILE__, __LINE__, "proportional drawChar case %" PRId32 " (bpp %" PRId32 ") mismatch at pixel %" PRId32, ci, bpp, bad);
    +        }
    +
    +        TEST_EQ(pen - c->x, textWidth(&sPropFont, c->text));
    +    }
    +
    +    // drawChar returns the glyph's advance, clipped or not
    +    setClipRect(&d, 0, 0, 0, 0);
    +    TEST_EQ(drawChar(&d, &ops, &sPropFont, 0, 0, 'W', fg, bg, true), sPropGlyphs['W'].advance);
    +    resetClipRect(&d);
    +    TEST_EQ(drawChar(&d, &ops, &sPropFont, 0, 0, 'i', fg, bg, true), sPropGlyphs['i'].advance);
    +
    +    // A character code past a font's glyphs takes its charWidth, drawn as
    +    // nothing
    +    BitmapFontT few = sPropFont;
    +
    +    few.numChars = 'A';
    +    primeSurface(&d, ref, pre);
    +    TEST_EQ(drawChar(&d, &ops, &few, 1, 1, 'W', fg, bg, false), PROP_DIGIT_W);
    +    TEST_EQ(textWidth(&few, "AW"), 2 * PROP_DIGIT_W);
    +    TEST_EQ(firstMismatch(&d, ref), -1);
    +
    +    // Scaled: each dot a block, the pen moving by scale times the advance
    +    const int32_t scales[] = { 2, 3 };
    +
    +    for (int32_t si = 0; si < DVX_ARRAY_LEN(scales); si++) {
    +        primeSurface(&d, ref, pre);
    +        setClipRect(&d, 2, 1, UNIT_SCREEN_W - 6, UNIT_SCREEN_H - 4);
    +        referenceTextProp(&d, &sPropFont, -3, 2, "gW\xFEj", scales[si], fg, fg, false, ref);
    +        drawTextScaled(&d, &ops, &sPropFont, -3, 2, "gW\xFEj", scales[si], fg);
    +        TEST_EQ(firstMismatch(&d, ref), -1);
    +    }
    +
    +    primeSurface(&d, ref, pre);
    +    drawTextScaled(&d, &ops, &sPropFont, 0, UNIT_SCREEN_H, "W", 2, fg);
    +    TEST_EQ(firstMismatch(&d, ref), -1);
    +
    +    resetClipRect(&d);
    +    free(ref);
    +    unitDisplayDestroy(&d);
    +}
    +
    +
     // drawTextScaled: each glyph dot a scale x scale block in fg over what is
     // there, against the reference at the same scale; partly off the surface,
     // clipped, scale 1 (drawText's transparent pixels) and below 1 (nothing).
    @@ -1145,6 +1485,118 @@ static void testTextScaled(int32_t bpp) {
     }
     
     
    +// drawTextStyled: the text, then an underline and / or strike-through as
    +// wide as it at the font's rows and thickness; bold and italic bits draw
    +// nothing more; drawTextAccel marks its key at the underline row under
    +// the key's advance
    +static void testTextStyled(int32_t bpp) {
    +    DisplayT  d;
    +    BlitOpsT  ops;
    +    uint32_t *ref = (uint32_t *)calloc((size_t)UNIT_SCREEN_W * UNIT_SCREEN_H, sizeof(uint32_t));
    +    uint32_t  fg  = colorForBpp(bpp, DRAW_FG);
    +    uint32_t  bg  = colorForBpp(bpp, DRAW_BG);
    +    uint32_t  pre = colorForBpp(bpp, DRAW_FILL);
    +    int32_t   x   = 3;
    +    int32_t   y   = 4;
    +    int32_t   w   = textWidth(&sPropFont, "AWg");
    +
    +    unitDisplayCreate(&d, UNIT_SCREEN_W, UNIT_SCREEN_H, bpp);
    +    drawInit(&ops, &d);
    +    resetClipRect(&d);
    +
    +    const uint32_t styles[] = { 0, FONT_STYLE_UNDERLINE, FONT_STYLE_STRIKE, FONT_STYLE_UNDERLINE | FONT_STYLE_STRIKE | FONT_STYLE_BOLD | FONT_STYLE_ITALIC };
    +
    +    for (int32_t si = 0; si < DVX_ARRAY_LEN(styles); si++) {
    +        primeSurface(&d, ref, pre);
    +        referenceTextProp(&d, &sPropFont, x, y, "AWg", 1, fg, bg, true, ref);
    +
    +        for (int32_t px = x; px < x + w; px++) {
    +            if (styles[si] & FONT_STYLE_UNDERLINE) {
    +                ref[(y + PROP_UNDERLINE_Y) * d.width + px] = fg;
    +            }
    +
    +            if (styles[si] & FONT_STYLE_STRIKE) {
    +                ref[(y + PROP_STRIKE_Y) * d.width + px] = fg;
    +            }
    +        }
    +
    +        drawTextStyled(&d, &ops, &sPropFont, x, y, "AWg", styles[si], fg, bg, true);
    +        TEST_EQ(firstMismatch(&d, ref), -1);
    +    }
    +
    +    // A font with lines draws them with every text call: drawText,
    +    // drawChar (across the advance), drawTextScaled (scaled); the
    +    // styled call adds only the lines the font lacks
    +    BitmapFontT lined = sPropFont;
    +
    +    lined.style = FONT_STYLE_UNDERLINE;
    +    primeSurface(&d, ref, pre);
    +    referenceTextProp(&d, &sPropFont, x, y, "AWg", 1, fg, bg, false, ref);
    +
    +    for (int32_t px = x; px < x + w; px++) {
    +        ref[(y + PROP_UNDERLINE_Y) * d.width + px] = fg;
    +    }
    +
    +    drawText(&d, &ops, &lined, x, y, "AWg", fg, bg, false);
    +    TEST_EQ(firstMismatch(&d, ref), -1);
    +
    +    for (int32_t px = x; px < x + w; px++) {
    +        ref[(y + PROP_STRIKE_Y) * d.width + px] = fg;
    +    }
    +
    +    drawTextStyled(&d, &ops, &lined, x, y, "AWg", FONT_STYLE_UNDERLINE | FONT_STYLE_STRIKE, fg, bg, false);
    +    TEST_EQ(firstMismatch(&d, ref), -1);
    +
    +    primeSurface(&d, ref, pre);
    +    referenceTextProp(&d, &sPropFont, x, y, "W", 1, fg, bg, true, ref);
    +
    +    for (int32_t px = x; px < x + sPropGlyphs['W'].advance; px++) {
    +        ref[(y + PROP_UNDERLINE_Y) * d.width + px] = fg;
    +    }
    +
    +    TEST_EQ(drawChar(&d, &ops, &lined, x, y, 'W', fg, bg, true), sPropGlyphs['W'].advance);
    +    TEST_EQ(firstMismatch(&d, ref), -1);
    +
    +    primeSurface(&d, ref, pre);
    +    referenceTextProp(&d, &sPropFont, x, y, "Ag", 2, fg, fg, false, ref);
    +
    +    for (int32_t py = y + PROP_UNDERLINE_Y * 2; py < y + PROP_UNDERLINE_Y * 2 + 2; py++) {
    +        for (int32_t px = x; px < x + textWidth(&sPropFont, "Ag") * 2; px++) {
    +            ref[py * d.width + px] = fg;
    +        }
    +    }
    +
    +    drawTextScaled(&d, &ops, &lined, x, y, "Ag", 2, fg);
    +    TEST_EQ(firstMismatch(&d, ref), -1);
    +
    +    // The system font's lines: underline on the cell's last row
    +    primeSurface(&d, ref, pre);
    +    referenceText(&d, x, y, "Ab", 1, fg, bg, false, ref);
    +
    +    for (int32_t px = x; px < x + 2 * FONT_CHAR_WIDTH; px++) {
    +        ref[(y + FONT_CHAR_HEIGHT - 1) * d.width + px] = fg;
    +        ref[(y + dvxFont8x16.strikeY) * d.width + px]  = fg;
    +    }
    +
    +    drawTextStyledN(&d, &ops, &dvxFont8x16, x, y, "Abc", 2, FONT_STYLE_UNDERLINE | FONT_STYLE_STRIKE, fg, bg, false);
    +    TEST_EQ(firstMismatch(&d, ref), -1);
    +
    +    // Accelerator: "&Wi" marks W across its 12-pixel advance
    +    primeSurface(&d, ref, pre);
    +    referenceTextProp(&d, &sPropFont, x, y, "Wi", 1, fg, bg, false, ref);
    +
    +    for (int32_t px = x; px < x + sPropGlyphs['W'].advance; px++) {
    +        ref[(y + PROP_UNDERLINE_Y) * d.width + px] = fg;
    +    }
    +
    +    drawTextAccel(&d, &ops, &sPropFont, x, y, "&Wi", fg, bg, false);
    +    TEST_EQ(firstMismatch(&d, ref), -1);
    +
    +    free(ref);
    +    unitDisplayDestroy(&d);
    +}
    +
    +
     static void testTextWidth(void) {
         TEST_EQ(dvxFont8x16.charWidth, FONT_CHAR_WIDTH);
         TEST_EQ(dvxFont8x16.charHeight, FONT_CHAR_HEIGHT);
    @@ -1183,6 +1635,33 @@ static void testTextWidth(void) {
     }
     
     
    +// Widths in a loaded font: the advances (not the bitmaps), counts of 0 and
    +// below, accelerator markers dropped
    +static void testTextWidthProp(void) {
    +    int32_t a = sPropGlyphs['A'].advance;
    +    int32_t w = sPropGlyphs['W'].advance;
    +    int32_t i = sPropGlyphs['i'].advance;
    +
    +    TEST_EQ(textWidth(&sPropFont, ""), 0);
    +    TEST_EQ(textWidth(&sPropFont, "AWi"), a + w + i);
    +    TEST_EQ(textWidth(&sPropFont, "?"), PROP_BLANK_ADV);
    +    TEST_EQ(textWidthN(&sPropFont, "AWi", 2), a + w);
    +    TEST_EQ(textWidthN(&sPropFont, "AWi", 0), 0);
    +    TEST_EQ(textWidthN(&sPropFont, "AWi", -1), 0);
    +    TEST_EQ(textWidthN(&dvxFont8x16, "AWi", 2), 2 * FONT_CHAR_WIDTH);
    +    TEST_EQ(textWidthN(&dvxFont8x16, "AWi", -3), 0);
    +    TEST_EQ(textWidthAccel(&sPropFont, "&Wi"), w + i);
    +    TEST_EQ(textWidthAccel(&sPropFont, "A&&W"), a + PROP_BLANK_ADV + w);
    +
    +    int32_t ox = 0;
    +    int32_t oy = 0;
    +
    +    calcCenteredText(&sPropFont, 10, 20, 100, 50, "AW", &ox, &oy);
    +    TEST_EQ(ox, 10 + (100 - (a + w)) / 2);
    +    TEST_EQ(oy, 20 + (50 - PROP_HEIGHT) / 2);
    +}
    +
    +
     // rectCopyTransparent skips key-coloured pixels (destination shows
     // through) and adjusts the source origin when the destination clips.
     static void testTransparent(int32_t bpp) {
    diff --git a/src/test/unit/testFlow.c b/src/test/unit/testFlow.c
    new file mode 100644
    index 00000000..939de3ca
    --- /dev/null
    +++ b/src/test/unit/testFlow.c
    @@ -0,0 +1,420 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// testFlow.c  -- unit section "flow": flowing text (dvxFlow.c)
    +//
    +// Layout in the system font (8-pixel cells: every width is a count of
    +// characters) and in a copy of it said to be taller (only its metrics are
    +// read for layout); painting compared, pixel for pixel, with the same text
    +// drawn by drawText where the flow should put it.
    +
    +#include "unitTest.h"
    +#include "dvxFlow.h"
    +#include "dvxDlg.h"
    +#include "dvxDraw.h"
    +#include "dvxFont.h"
    +#include "dvxVideo.h"
    +
    +#include 
    +#include 
    +
    +#define FLOW_BPP          32
    +#define FLOW_CW           FONT_CHAR_WIDTH
    +#define FLOW_LINE         FONT_CHAR_HEIGHT
    +#define FLOW_TALL_LINE    24         // the taller font's line
    +#define FLOW_TALL_ASCENT  18
    +#define FLOW_FG           0x00FFFFFFu
    +#define FLOW_BG           0x00000000u
    +#define FLOW_RED          0x00FF0000u
    +#define FLOW_BOX_W        40         // an aligned paragraph's width
    +#define FLOW_GROWN_LINE   20         // a rebuilt font's line
    +#define FLOW_WRAP_COLS    10         // "Hello world foo" wraps after "Hello "
    +#define FLOW_SEL_BG       0x000000FFu
    +#define FLOW_FAR          200        // a hit test right of every line
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +static bool sameSurface(const DisplayT *a, const DisplayT *b);
    +static void testAlignAndPaint(void);
    +static void testBreaks(void);
    +static void testEditing(void);
    +static void testEditingPaint(void);
    +static void testEmpty(void);
    +void        testFlow(void);
    +static void testMixedHeights(void);
    +static void testRebuilt(void);
    +static void testWrap(void);
    +
    +
    +// Every pixel of a equals b's
    +static bool sameSurface(const DisplayT *a, const DisplayT *b) {
    +    for (int32_t y = 0; y < a->height; y++) {
    +        for (int32_t x = 0; x < a->width; x++) {
    +            if (unitPixelGet(a, x, y) != unitPixelGet(b, x, y)) {
    +                return false;
    +            }
    +        }
    +    }
    +
    +    return true;
    +}
    +
    +
    +// Painting: aligned within the width laid out at, each run in its color,
    +// trailing spaces not drawn (an underlined font shows it)
    +static void testAlignAndPaint(void) {
    +    DisplayT    got;
    +    DisplayT    want;
    +    BlitOpsT    ops;
    +    BitmapFontT lined = dvxFont8x16;
    +    TextFlowT  *flow  = dvxFlowCreate();
    +    uint32_t    fg;
    +    uint32_t    red;
    +
    +    unitDisplayCreate(&got, UNIT_SCREEN_W, UNIT_SCREEN_H, FLOW_BPP);
    +    unitDisplayCreate(&want, UNIT_SCREEN_W, UNIT_SCREEN_H, FLOW_BPP);
    +    drawInit(&ops, &got);
    +    fg  = packColor(&got, DVX_RGB_RED(FLOW_FG), DVX_RGB_GREEN(FLOW_FG), DVX_RGB_BLUE(FLOW_FG));
    +    red = packColor(&got, DVX_RGB_RED(FLOW_RED), DVX_RGB_GREEN(FLOW_RED), DVX_RGB_BLUE(FLOW_RED));
    +
    +    // Centered "ab" in 40 pixels, then a right-aligned red "c"
    +    dvxFlowAddParagraph(flow, FLOW_ALIGN_CENTER);
    +    dvxFlowAddText(flow, "ab", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    dvxFlowAddParagraph(flow, FLOW_ALIGN_RIGHT);
    +    dvxFlowAddText(flow, "c", -1, (BitmapFontT *)&dvxFont8x16, FLOW_RED);
    +    TEST_EQ(dvxFlowLayout(flow, FLOW_BOX_W), 2 * FLOW_LINE);
    +
    +    rectFill(&got, &ops, 0, 0, got.width, got.height, 0);
    +    rectFill(&want, &ops, 0, 0, want.width, want.height, 0);
    +    dvxFlowPaint(flow, &got, &ops, 2, 3, fg);
    +    drawText(&want, &ops, &dvxFont8x16, 2 + (FLOW_BOX_W - 2 * FLOW_CW) / 2, 3, "ab", fg, 0, false);
    +    drawText(&want, &ops, &dvxFont8x16, 2 + FLOW_BOX_W - FLOW_CW, 3 + FLOW_LINE, "c", red, 0, false);
    +    TEST_TRUE(sameSurface(&got, &want));
    +
    +    // Trailing spaces at a wrap draw nothing: the underline stops at the word
    +    lined.style = FONT_STYLE_UNDERLINE;
    +    dvxFlowClear(flow);
    +    dvxFlowAddText(flow, "ab   cd", -1, &lined, FLOW_COLOR_DEFAULT);
    +    TEST_EQ(dvxFlowLayout(flow, 3 * FLOW_CW), 2 * FLOW_LINE);
    +    rectFill(&got, &ops, 0, 0, got.width, got.height, 0);
    +    rectFill(&want, &ops, 0, 0, want.width, want.height, 0);
    +    dvxFlowPaint(flow, &got, &ops, 0, 0, fg);
    +    drawText(&want, &ops, &lined, 0, 0, "ab", fg, 0, false);
    +    drawText(&want, &ops, &lined, 0, FLOW_LINE, "cd", fg, 0, false);
    +    TEST_TRUE(sameSurface(&got, &want));
    +
    +    // Lines outside the clip rect are skipped, those in it drawn
    +    setClipRect(&got, 0, FLOW_LINE, got.width, FLOW_LINE);
    +    setClipRect(&want, 0, FLOW_LINE, want.width, FLOW_LINE);
    +    rectFill(&got, &ops, 0, 0, got.width, got.height, 0);
    +    rectFill(&want, &ops, 0, 0, want.width, want.height, 0);
    +    dvxFlowPaint(flow, &got, &ops, 0, 0, fg);
    +    drawText(&want, &ops, &lined, 0, FLOW_LINE, "cd", fg, 0, false);
    +    TEST_TRUE(sameSurface(&got, &want));
    +
    +    dvxFlowDestroy(flow);
    +    unitDisplayDestroy(&got);
    +    unitDisplayDestroy(&want);
    +}
    +
    +
    +// Breaks start lines (two make an empty one); a paragraph ends a line
    +// only when the line has text; a break at the very end adds nothing
    +static void testBreaks(void) {
    +    TextFlowT *flow = dvxFlowCreate();
    +
    +    dvxFlowAddText(flow, "a", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    dvxFlowAddBreak(flow);
    +    dvxFlowAddBreak(flow);
    +    dvxFlowAddText(flow, "b", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    TEST_EQ(dvxFlowLayout(flow, FLOW_WIDTH_NATURAL), 3 * FLOW_LINE);
    +
    +    dvxFlowClear(flow);
    +    dvxFlowAddText(flow, "a", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    dvxFlowAddBreak(flow);
    +    dvxFlowAddParagraph(flow, FLOW_ALIGN_RIGHT);
    +    dvxFlowAddText(flow, "b", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    dvxFlowAddBreak(flow);
    +    TEST_EQ(dvxFlowLayout(flow, FLOW_WIDTH_NATURAL), 2 * FLOW_LINE);
    +
    +    // A change after layout lays out again at the same width
    +    dvxFlowAddText(flow, "c", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    TEST_EQ(dvxFlowLayout(flow, FLOW_WIDTH_NATURAL), 3 * FLOW_LINE);
    +    dvxFlowDestroy(flow);
    +}
    +
    +
    +// What the rich edit box needs: offsets through runs, each line's range,
    +// the caret's place, hit tests (a wrapped line ends before its space, a
    +// word broken across lines at the next line's start), an empty flow's one
    +// line, alignment, and painting a selection
    +static void testEditing(void) {
    +    TextFlowT *flow = dvxFlowCreate();
    +    int32_t    x    = 0;
    +    int32_t    y    = 0;
    +    int32_t    h    = 0;
    +
    +    dvxFlowAddText(flow, "Hello ", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    dvxFlowAddText(flow, "world foo", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    TEST_EQ(dvxFlowTextLength(flow), 15);
    +    TEST_EQ(dvxFlowLineCount(flow), 0);
    +    TEST_EQ(dvxFlowLayout(flow, FLOW_WRAP_COLS * FLOW_CW), 2 * FLOW_LINE);
    +    TEST_EQ(dvxFlowLineCount(flow), 2);
    +    TEST_EQ(dvxFlowLineStart(flow, 0), 0);
    +    TEST_EQ(dvxFlowLineStart(flow, 1), 6);
    +    TEST_EQ(dvxFlowLineStart(flow, 2), 15);
    +    TEST_EQ(dvxFlowLineOf(flow, 5), 0);
    +    TEST_EQ(dvxFlowLineOf(flow, 6), 1);
    +    TEST_EQ(dvxFlowLineOf(flow, 15), 1);
    +    dvxFlowLineBox(flow, 1, &y, &h);
    +    TEST_EQ(y, FLOW_LINE);
    +    TEST_EQ(h, FLOW_LINE);
    +    dvxFlowLineBox(flow, 2, &y, &h);
    +    TEST_EQ(h, 0);
    +
    +    // The caret: in a run, across runs, in the undrawn trailing space
    +    dvxFlowCaret(flow, 3, &x, &y, &h);
    +    TEST_EQ(x, 3 * FLOW_CW);
    +    TEST_EQ(y, 0);
    +    TEST_EQ(h, FLOW_LINE);
    +    dvxFlowCaret(flow, 8, &x, &y, &h);
    +    TEST_EQ(x, 2 * FLOW_CW);
    +    TEST_EQ(y, FLOW_LINE);
    +    dvxFlowCaret(flow, 5, &x, &y, &h);
    +    TEST_EQ(x, 5 * FLOW_CW);
    +
    +    // Hit tests: the nearest boundary, a wrapped line's end, beyond the
    +    // lines
    +    TEST_EQ(dvxFlowHitTest(flow, FLOW_CW + FLOW_CW / 2 + 1, 0), 2);
    +    TEST_EQ(dvxFlowHitTest(flow, FLOW_CW + FLOW_CW / 2 - 1, 0), 1);
    +    TEST_EQ(dvxFlowHitTest(flow, FLOW_FAR, 0), 5);
    +    TEST_EQ(dvxFlowHitTest(flow, FLOW_FAR, FLOW_LINE), 15);
    +    TEST_EQ(dvxFlowHitTest(flow, -1, FLOW_FAR), 6);
    +    TEST_EQ(dvxFlowHitTest(flow, -1, -1), 0);
    +
    +    // A word broken across lines: the line ends at the next one's start
    +    dvxFlowClear(flow);
    +    dvxFlowAddText(flow, "abcdefghij", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    TEST_EQ(dvxFlowLayout(flow, 4 * FLOW_CW), 3 * FLOW_LINE);
    +    TEST_EQ(dvxFlowLineStart(flow, 1), 4);
    +    TEST_EQ(dvxFlowHitTest(flow, FLOW_FAR, 0), 4);
    +
    +    // Empty text: one line in its font; nothing at all: none
    +    dvxFlowClear(flow);
    +    dvxFlowAddText(flow, "", 0, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    TEST_EQ(dvxFlowLayout(flow, FLOW_BOX_W), FLOW_LINE);
    +    TEST_EQ(dvxFlowLineCount(flow), 1);
    +    TEST_EQ(dvxFlowHitTest(flow, FLOW_FAR, FLOW_FAR), 0);
    +    dvxFlowClear(flow);
    +    TEST_EQ(dvxFlowLayout(flow, FLOW_BOX_W), 0);
    +    TEST_EQ(dvxFlowLineOf(flow, 0), -1);
    +    dvxFlowCaret(flow, 0, &x, &y, &h);
    +    TEST_EQ(h, 0);
    +    TEST_EQ(dvxFlowHitTest(flow, 0, 0), 0);
    +
    +    // Centered: the caret and hit tests move with the line
    +    dvxFlowAddParagraph(flow, FLOW_ALIGN_CENTER);
    +    dvxFlowAddText(flow, "ab", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    dvxFlowLayout(flow, FLOW_BOX_W);
    +    dvxFlowCaret(flow, 0, &x, &y, &h);
    +    TEST_EQ(x, (FLOW_BOX_W - 2 * FLOW_CW) / 2);
    +    TEST_EQ(dvxFlowHitTest(flow, x + FLOW_CW, 0), 1);
    +
    +    dvxFlowDestroy(flow);
    +    testEditingPaint();
    +}
    +
    +
    +// A selection paints its text in its own color on its background, the
    +// rest as dvxFlowPaint; across a wrap the background covers the
    +// undrawn trailing spaces
    +static void testEditingPaint(void) {
    +    DisplayT   got;
    +    DisplayT   want;
    +    BlitOpsT   ops;
    +    TextFlowT *flow = dvxFlowCreate();
    +
    +    unitDisplayCreate(&got, UNIT_SCREEN_W, UNIT_SCREEN_H, FLOW_BPP);
    +    unitDisplayCreate(&want, UNIT_SCREEN_W, UNIT_SCREEN_H, FLOW_BPP);
    +    drawInit(&ops, &got);
    +
    +    uint32_t fg  = packColor(&got, DVX_RGB_RED(FLOW_FG), DVX_RGB_GREEN(FLOW_FG), DVX_RGB_BLUE(FLOW_FG));
    +    uint32_t red = packColor(&got, DVX_RGB_RED(FLOW_RED), DVX_RGB_GREEN(FLOW_RED), DVX_RGB_BLUE(FLOW_RED));
    +    uint32_t sel = packColor(&got, DVX_RGB_RED(FLOW_SEL_BG), DVX_RGB_GREEN(FLOW_SEL_BG), DVX_RGB_BLUE(FLOW_SEL_BG));
    +
    +    dvxFlowAddText(flow, "ab", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    dvxFlowAddText(flow, "cd", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    dvxFlowLayout(flow, FLOW_BOX_W);
    +    rectFill(&got, &ops, 0, 0, got.width, got.height, 0);
    +    rectFill(&want, &ops, 0, 0, want.width, want.height, 0);
    +    dvxFlowPaintSel(flow, &got, &ops, 0, 0, fg, 1, 3, red, sel);
    +    rectFill(&want, &ops, FLOW_CW, 0, 2 * FLOW_CW, FLOW_LINE, sel);
    +    drawText(&want, &ops, &dvxFont8x16, 0, 0, "a", fg, 0, false);
    +    drawText(&want, &ops, &dvxFont8x16, FLOW_CW, 0, "bc", red, 0, false);
    +    drawText(&want, &ops, &dvxFont8x16, 3 * FLOW_CW, 0, "d", fg, 0, false);
    +    TEST_TRUE(sameSurface(&got, &want));
    +
    +    // "ab  " wraps before "cd": selecting it all lights the spaces too
    +    dvxFlowClear(flow);
    +    dvxFlowAddText(flow, "ab  cd", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    dvxFlowLayout(flow, 3 * FLOW_CW);
    +    rectFill(&got, &ops, 0, 0, got.width, got.height, 0);
    +    rectFill(&want, &ops, 0, 0, want.width, want.height, 0);
    +    dvxFlowPaintSel(flow, &got, &ops, 0, 0, fg, 0, 4, red, sel);
    +    rectFill(&want, &ops, 0, 0, 4 * FLOW_CW, FLOW_LINE, sel);
    +    drawText(&want, &ops, &dvxFont8x16, 0, 0, "ab", red, 0, false);
    +    drawText(&want, &ops, &dvxFont8x16, 0, FLOW_LINE, "cd", fg, 0, false);
    +    TEST_TRUE(sameSurface(&got, &want));
    +
    +    dvxFlowDestroy(flow);
    +    unitDisplayDestroy(&got);
    +    unitDisplayDestroy(&want);
    +}
    +
    +
    +// Nothing in: no size; NULL everywhere ignored
    +static void testEmpty(void) {
    +    TextFlowT *flow = dvxFlowCreate();
    +
    +    TEST_PTR_NOT_NULL(flow);
    +    TEST_EQ(dvxFlowLayout(flow, FLOW_WIDTH_NATURAL), 0);
    +    TEST_EQ(dvxFlowWidth(flow), 0);
    +    TEST_EQ(dvxFlowHeight(flow), 0);
    +    TEST_FALSE(dvxFlowAddText(flow, NULL, 0, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT));
    +    TEST_FALSE(dvxFlowAddText(flow, "x", -1, NULL, FLOW_COLOR_DEFAULT));
    +    TEST_FALSE(dvxFlowAddBreak(NULL));
    +    TEST_EQ(dvxFlowLayout(NULL, 1), 0);
    +    dvxFlowClear(NULL);
    +    dvxFlowDestroy(NULL);
    +    dvxFlowDestroy(flow);
    +}
    +
    +
    +void testFlow(void) {
    +    testEmpty();
    +    testWrap();
    +    testBreaks();
    +    testMixedHeights();
    +    testAlignAndPaint();
    +    testRebuilt();
    +    testEditing();
    +}
    +
    +
    +// A line is as tall as its tallest font: the taller ascent above the
    +// baseline and the deeper descent below it
    +static void testMixedHeights(void) {
    +    BitmapFontT tall = dvxFont8x16;
    +    TextFlowT  *flow = dvxFlowCreate();
    +
    +    tall.charHeight = FLOW_TALL_LINE;
    +    tall.ascent     = FLOW_TALL_ASCENT;
    +    dvxFlowAddText(flow, "ab", -1, &tall, FLOW_COLOR_DEFAULT);
    +    dvxFlowAddText(flow, "cd", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    TEST_EQ(dvxFlowLayout(flow, FLOW_WIDTH_NATURAL), FLOW_TALL_ASCENT + DVX_MAX(FLOW_TALL_LINE - FLOW_TALL_ASCENT, FLOW_LINE - FONT_SYS_ASCENT));
    +    TEST_EQ(dvxFlowWidth(flow), 4 * FLOW_CW);
    +
    +    // An empty line is as tall as the font before it
    +    dvxFlowAddBreak(flow);
    +    dvxFlowAddBreak(flow);
    +    dvxFlowAddText(flow, "e", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    TEST_EQ(dvxFlowLayout(flow, FLOW_WIDTH_NATURAL), FLOW_TALL_LINE + 2 * FLOW_LINE);
    +    dvxFlowDestroy(flow);
    +}
    +
    +
    +// A run's font built again (a new serial: dvxFontSetSmooth) is measured
    +// again at the width laid out before; without one the layout is kept
    +static void testRebuilt(void) {
    +    BitmapFontT font = dvxFont8x16;
    +    TextFlowT  *flow = dvxFlowCreate();
    +
    +    dvxFlowAddText(flow, "ab", -1, &font, FLOW_COLOR_DEFAULT);
    +    TEST_EQ(dvxFlowLayout(flow, FLOW_WIDTH_NATURAL), FLOW_LINE);
    +    font.charHeight = FLOW_GROWN_LINE;
    +    TEST_EQ(dvxFlowLayout(flow, FLOW_WIDTH_NATURAL), FLOW_LINE);
    +    font.serial++;
    +    TEST_EQ(dvxFlowLayout(flow, FLOW_WIDTH_NATURAL), FLOW_GROWN_LINE);
    +    TEST_EQ(dvxFlowHeight(flow), FLOW_GROWN_LINE);
    +    dvxFlowDestroy(flow);
    +}
    +
    +
    +// Wrapping at spaces (trailing spaces not counted), a word too wide for
    +// a line broken where the line is full, words across runs, the natural
    +// width
    +static void testWrap(void) {
    +    TextFlowT *flow = dvxFlowCreate();
    +
    +    dvxFlowAddText(flow, "Hello world", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    TEST_EQ(dvxFlowLayout(flow, FLOW_WIDTH_NATURAL), FLOW_LINE);
    +    TEST_EQ(dvxFlowWidth(flow), 11 * FLOW_CW);
    +    TEST_EQ(dvxFlowLayout(flow, 8 * FLOW_CW), 2 * FLOW_LINE);
    +    TEST_EQ(dvxFlowWidth(flow), 5 * FLOW_CW);
    +    TEST_EQ(dvxFlowHeight(flow), 2 * FLOW_LINE);
    +
    +    // Exactly fitting words stay on the line
    +    TEST_EQ(dvxFlowLayout(flow, 11 * FLOW_CW), FLOW_LINE);
    +
    +    dvxFlowClear(flow);
    +    dvxFlowAddText(flow, "abcdefghij", 7, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    TEST_EQ(dvxFlowLayout(flow, 3 * FLOW_CW), 3 * FLOW_LINE);
    +    TEST_EQ(dvxFlowWidth(flow), 3 * FLOW_CW);
    +
    +    // Narrower than one character: one per line
    +    TEST_EQ(dvxFlowLayout(flow, 1), 7 * FLOW_LINE);
    +
    +    // Spaces that start a wrapped line are dropped; a run ending in spaces
    +    // followed by another wraps between them
    +    dvxFlowClear(flow);
    +    dvxFlowAddText(flow, "aa  ", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    dvxFlowAddText(flow, "  bb", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    TEST_EQ(dvxFlowLayout(flow, 3 * FLOW_CW), 2 * FLOW_LINE);
    +    TEST_EQ(dvxFlowWidth(flow), 2 * FLOW_CW);
    +
    +    // A word going on into the next run wraps whole: "bb" + "b," moves to
    +    // the second line together, "cc" to the third
    +    dvxFlowClear(flow);
    +    dvxFlowAddText(flow, "aaa bb", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    dvxFlowAddText(flow, "b, cc", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    TEST_EQ(dvxFlowLayout(flow, 6 * FLOW_CW), 3 * FLOW_LINE);
    +    TEST_EQ(dvxFlowWidth(flow), 4 * FLOW_CW);
    +    TEST_EQ(dvxFlowLineStart(flow, 1), 4);
    +    TEST_EQ(dvxFlowLineStart(flow, 2), 9);
    +
    +    // A line break ends the word: "bb" stays
    +    dvxFlowClear(flow);
    +    dvxFlowAddText(flow, "aaa bb", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    dvxFlowAddBreak(flow);
    +    dvxFlowAddText(flow, "cc", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    TEST_EQ(dvxFlowLayout(flow, 6 * FLOW_CW), 2 * FLOW_LINE);
    +
    +    // A word across runs wider than a whole line breaks between them
    +    dvxFlowClear(flow);
    +    dvxFlowAddText(flow, "abc", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    dvxFlowAddText(flow, "def", -1, (BitmapFontT *)&dvxFont8x16, FLOW_COLOR_DEFAULT);
    +    TEST_EQ(dvxFlowLayout(flow, 4 * FLOW_CW), 2 * FLOW_LINE);
    +    TEST_EQ(dvxFlowLineStart(flow, 1), 3);
    +    dvxFlowDestroy(flow);
    +}
    diff --git a/src/test/unit/testFonts.c b/src/test/unit/testFonts.c
    new file mode 100644
    index 00000000..fadc329a
    --- /dev/null
    +++ b/src/test/unit/testFonts.c
    @@ -0,0 +1,1095 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// testFonts.c  -- unit section "fonts": the font families and loaded fonts
    +// (dvxText.c) and their BASIC entry points (dvxTextBas.c)
    +//
    +// The section works in a scratch folder of its own holding a FONTS folder
    +// of .DFN files it writes (dvxFontFmt.h): two families of its own, small
    +// stand-ins for the fallback families (so what fills in a missing
    +// character is known), and files the scan must skip.  Every glyph of a
    +// written face has a bitmap no other glyph shares, so where a loaded
    +// glyph came from shows.  Fonts added for a program are written beside
    +// the FONTS folder (and into a stand-in program file as font resources).
    +
    +#include "unitTest.h"
    +#include "dvxText.h"
    +#include "dvxDraw.h"
    +#include "dvxFont.h"
    +#include "dvxFontFmt.h"
    +#include "dvxMem.h"
    +#include "dvxRes.h"
    +#include "dvxVideo.h"
    +#include "dvxWgt.h"
    +
    +#include 
    +#include 
    +#include 
    +#include 
    +#include 
    +#include 
    +
    +#define TEXT_DIR           "fontsTest"
    +#define TEXT_DIR_MODE      0755
    +#define TEXT_SIZES_MAX     4
    +#define TEXT_STRIKES_MAX   (TEXT_SIZES_MAX * 2)   // each size's 1-bit strike and its smoothed one
    +#define TEXT_BITS_MAX      4096
    +#define TEXT_APP_ID        7         // a program's id while it opens fonts
    +#define TEXT_OTHER_APP_ID  8         // another program's
    +#define TEXT_NO_APP_ID     (-2)      // no program's
    +#define TEXT_PROGRAM       "PROG.APP"     // a stand-in program file with font resources
    +#define TEXT_PROGRAM_CODE  "not really code"
    +#define TEXT_TEXT_MAX      64        // a written face's notice or licence text
    +#define TEXT_ASCENT_NUM    2         // a written glyph is this fraction of the size tall
    +#define TEXT_ASCENT_DEN    3
    +#define TEXT_ADVANCE_STEP  3         // proportional advances vary with the code (mod this)
    +#define TEXT_STYLE_MARK    3         // a glyph's second row: its size plus this times its face's style (low bits, inside any glyph)
    +#define TEXT_GRAY_SOFT     1         // a smoothed glyph's level past its first two rows (one more in odd columns)
    +#define TEXT_GRAY_X        2         // where the smoothed drawing goes
    +#define TEXT_GRAY_Y        3
    +#define CH_SMILEY          0x01
    +#define CH_BOX_H           0xC4      // box drawing: horizontal line
    +#define CH_BLOCK_FULL      0xDB
    +#define CH_NBSP            0xFF
    +
    +// A face to write: FONTS\
    +typedef struct {
    +    const char *file;
    +    const char *family;
    +    uint16_t    style;
    +    uint16_t    faceFlags;
    +    int32_t     sizes[TEXT_SIZES_MAX];
    +    int32_t     sizeCount;
    +    uint32_t    smoothMask;          // bit i: sizes[i] smoothed too
    +    const char *chars;               // the characters it has (it always has a space)
    +} FaceDefT;
    +
    +static const FaceDefT sFaces[] = {
    +    { "TSANS.DFN",    "Test Sans",        0,                                   0,                                  { 8, 12 }, 2, 0x2, "0AxCDEF" },
    +    { "TSANSB.DFN",   "Test Sans",        FONT_STYLE_BOLD,                     0,                                  { 12 },    1, 0,   "0AB"     },
    +    { "TMONO.DFN",    "Test Mono",        0,                                   DFN_FACE_FIXED | DFN_FACE_BOXDRAW,  { 10 },    1, 0,   "0A\xC4"  },
    +    { "DJVSANS.DFN",  "DejaVu Sans",      0,                                   0,                                  { 8, 12 }, 2, 0,   "0B\x01"  },
    +    { "DJVSANSB.DFN", "DejaVu Sans",      FONT_STYLE_BOLD,                     0,                                  { 12 },    1, 0,   "0BQ"     },
    +    { "DJVMONO.DFN",  "DejaVu Sans Mono", 0,                                   DFN_FACE_FIXED,                     { 10 },    1, 0,   "B"       },
    +    { "ZZDUP.DFN",    "Test Sans",        0,                                   0,                                  { 20 },    1, 0,   "Z"       },
    +    { "SYSTEM.DFN",   "System",           0,                                   0,                                  { 9 },     1, 0,   "S"       },
    +    { "BADSTRK.DFN",  "Bad Strike",       0,                                   0,                                  { 10 },    1, 0,   "A"       }
    +};
    +
    +#define FACE_BAD_STRIKE    8         // sFaces index of the face whose glyph sizes are spoiled
    +
    +// Faces added for a program (written beside FONTS, not in it): one of an
    +// installed family (its own sizes, no 'C', a 'Z' the installed face
    +// lacks, nothing smoothed), a family only it has, regular and bold, and
    +// one added from memory
    +static const FaceDefT sAppSans = { "APPSANS.DFN", "Test Sans", 0,               0, { 12, 40 }, 2, 0, "0AZ" };
    +static const FaceDefT sAppOnly = { "APPONLY.DFN", "App Only",  0,               0, { 10 },     1, 0, "0A"  };
    +static const FaceDefT sAppBold = { "APPB.DFN",    "App Only",  FONT_STYLE_BOLD, 0, { 10 },     1, 0, "0A"  };
    +static const FaceDefT sMemFace = { "MEM.DFN",     "Mem Font",  0,               0, { 9 },      1, 0, "0M"  };
    +
    +static const BitmapFontT *sPaintedWith;   // what recordPaint was handed
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +int32_t         FontAddData(const char *name, const uint8_t *data, int32_t len);
    +int32_t         FontAddFile(const char *path);
    +int32_t         FontAddResources(const char *path);
    +int32_t         FontAscent(const char *family, int32_t size, int32_t style);
    +int32_t         FontFamilyCount(void);
    +int32_t         FontFamilyFixed(const char *family);
    +const char     *FontFamilyName(int32_t index);
    +const char     *FontLicence(const char *family, int32_t size, int32_t style);
    +int32_t         FontLineHeight(const char *family, int32_t size, int32_t style);
    +int32_t         FontNearestSize(const char *family, int32_t size, int32_t style);
    +void            FontRescan(void);
    +int32_t         FontSizeAt(const char *family, int32_t style, int32_t index);
    +int32_t         FontSizeCount(const char *family, int32_t style);
    +int32_t         FontTextWidth(const char *family, int32_t size, int32_t style, const char *text);
    +static bool     glyphSame(const BitmapFontT *a, int32_t ca, const BitmapFontT *b, int32_t cb);
    +static bool     hasChar(const FaceDefT *def, int32_t ch);
    +static int32_t  levelAt(const BitmapFontT *font, int32_t ch, int32_t row, int32_t x);
    +static uint8_t *readAll(const char *path, int32_t *len);
    +static void     recordPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
    +static bool     rowSet(const BitmapFontT *font, int32_t ch, int32_t row, int32_t from, int32_t to);
    +static void     strikeMake(const FaceDefT *def, int32_t size, bool gray, DfnStrikeT *strike, FontGlyphT *glyphs, uint8_t *bits);
    +static void     testAdded(void);
    +static void     testBasic(void);
    +static void     testCache(void);
    +static void     testDetachOldest(void);
    +static void     testFamilies(void);
    +static void     testFill(void);
    +static void     testFixed(void);
    +void            testFonts(void);
    +static void     testNoFallback(void);
    +static void     testSmooth(void);
    +static void     testSmoothDraw(BitmapFontT *font, int32_t bpp);
    +static void     testStyles(void);
    +static void     testSystem(void);
    +static void     testWidgets(void);
    +static void     writeFace(const FaceDefT *def, bool spoil);
    +static void     writeFaceAt(const FaceDefT *def, bool spoil, const char *path);
    +static void     writeFile(const char *name, const void *data, size_t len);
    +
    +
    +// Glyph ca of a and cb of b are the same picture at the same place
    +static bool glyphSame(const BitmapFontT *a, int32_t ca, const BitmapFontT *b, int32_t cb) {
    +    const FontGlyphT *ga = &a->glyphs[ca];
    +    const FontGlyphT *gb = &b->glyphs[cb];
    +
    +    if (ga->width != gb->width || ga->height != gb->height || ga->top != gb->top) {
    +        return false;
    +    }
    +
    +    return memcmp(a->glyphData + a->glyphAt[ca], b->glyphData + b->glyphAt[cb], FONT_GLYPH_BYTES(ga)) == 0;
    +}
    +
    +
    +static bool hasChar(const FaceDefT *def, int32_t ch) {
    +    return ch == ' ' || (ch != 0 && strchr(def->chars, ch) != NULL);
    +}
    +
    +
    +// The level of pixel x of glyph ch's row row in a smoothed font
    +static int32_t levelAt(const BitmapFontT *font, int32_t ch, int32_t row, int32_t x) {
    +    const FontGlyphT *g = &font->glyphs[ch];
    +
    +    return FONT_GRAY_LEVEL(font->glyphData + font->glyphAt[ch] + row * FONT_ROW_BYTES_BPP(g->width, FONT_GRAY_BPP), x);
    +}
    +
    +
    +// The whole file at path (malloc'd), its length in *len; NULL when it
    +// cannot be read
    +static uint8_t *readAll(const char *path, int32_t *len) {
    +    FILE *fp = fopen(path, "rb");
    +
    +    if (!fp) {
    +        return NULL;
    +    }
    +
    +    fseek(fp, 0, SEEK_END);
    +    *len = (int32_t)ftell(fp);
    +    fseek(fp, 0, SEEK_SET);
    +
    +    uint8_t *data = (uint8_t *)malloc((size_t)*len);
    +
    +    if (data && fread(data, 1, (size_t)*len, fp) != (size_t)*len) {
    +        free(data);
    +        data = NULL;
    +    }
    +
    +    fclose(fp);
    +    return data;
    +}
    +
    +
    +// A paint method that keeps the font it is handed
    +static void recordPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
    +    (void)w;
    +    (void)d;
    +    (void)ops;
    +    (void)colors;
    +    sPaintedWith = font;
    +}
    +
    +
    +// Every pixel from column from to column to (exclusive) of glyph ch's row
    +// row is set
    +static bool rowSet(const BitmapFontT *font, int32_t ch, int32_t row, int32_t from, int32_t to) {
    +    const FontGlyphT *g    = &font->glyphs[ch];
    +    const uint8_t    *bits = font->glyphData + font->glyphAt[ch] + row * FONT_ROW_BYTES(g->width);
    +
    +    for (int32_t x = from; x < to; x++) {
    +        if (!(bits[FONT_PIXEL_BYTE(x)] & FONT_PIXEL_BIT(x))) {
    +            return false;
    +        }
    +    }
    +
    +    return true;
    +}
    +
    +
    +// Strike size of face def, 1-bit or gray, into strike (its offsets
    +// left to the caller), glyphs and bits
    +static void strikeMake(const FaceDefT *def, int32_t size, bool gray, DfnStrikeT *strike, FontGlyphT *glyphs, uint8_t *bits) {
    +    int32_t  height = size * TEXT_ASCENT_NUM / TEXT_ASCENT_DEN;
    +    int32_t  pitch  = size / 2;
    +    int32_t  bpp    = gray ? FONT_GRAY_BPP : 1;
    +    uint32_t used   = 0;
    +
    +    for (int32_t ch = 0; ch < DFN_CHAR_COUNT; ch++) {
    +        FontGlyphT *g = &glyphs[ch];
    +
    +        if (!hasChar(def, ch)) {
    +            g->flags = FONT_GLYPH_MISSING;
    +            continue;
    +        }
    +
    +        g->advance = (uint8_t)((def->faceFlags & DFN_FACE_FIXED) ? pitch : pitch + ch % TEXT_ADVANCE_STEP);
    +        g->width   = (uint8_t)((ch == ' ') ? 0 : g->advance - 1);
    +        g->height  = (uint8_t)((ch == ' ') ? 0 : height);
    +        g->top     = (int8_t)g->height;
    +
    +        // A picture only this character, face and size has: its code
    +        // in the first row, the face's style and size in the second,
    +        // the rest solid (smoothed: soft, every other column softer)
    +        int32_t rowBytes = FONT_ROW_BYTES_BPP(g->width, bpp);
    +
    +        for (int32_t y = 0; y < g->height; y++) {
    +            uint8_t *row = bits + used + y * rowBytes;
    +
    +            for (int32_t x = 0; x < g->width; x++) {
    +                uint32_t mark = (y == 0) ? (uint32_t)ch : (y == 1) ? (uint32_t)(size + def->style * TEXT_STYLE_MARK) : UINT32_MAX;
    +
    +                if (!((mark >> (x % 16)) & 1)) {
    +                    continue;
    +                }
    +
    +                if (gray) {
    +                    row[x / FONT_GRAY_PER_BYTE] |= (uint8_t)(((y > 1) ? TEXT_GRAY_SOFT + x % 2 : FONT_GRAY_FULL) << FONT_GRAY_SHIFT(x));
    +                } else {
    +                    row[FONT_PIXEL_BYTE(x)] |= FONT_PIXEL_BIT(x);
    +                }
    +            }
    +        }
    +
    +        used += FONT_GLYPH_BYTES_BPP(g, bpp);
    +    }
    +
    +    strike->pixelSize   = (uint16_t)size;
    +    strike->flags       = gray ? DFN_STRIKE_GRAY2 : 0;
    +    strike->ascent      = (int16_t)height;
    +    strike->descent     = (int16_t)(size - height);
    +    strike->lineHeight  = (int16_t)size;
    +    strike->maxAdvance  = (uint16_t)(pitch + TEXT_ADVANCE_STEP);
    +    strike->bitmapBytes = used;
    +}
    +
    +
    +// Fonts added for a program: it sees them, merged with the installed
    +// family by style and size (its faces and sizes first); another program
    +// does not; added from a file, a program's font resources, or memory
    +// (copied); gone when it ends, its held fonts still drawing; and the
    +// same for fonts added for every program
    +static void testAdded(void) {
    +    int32_t  app   = TEXT_APP_ID;
    +    int32_t  other = TEXT_OTHER_APP_ID;
    +    int32_t *prev  = dvxMemAppIdPtr;
    +    int32_t  sizes[TEXT_SIZES_MAX];
    +    int32_t  len   = 0;
    +
    +    writeFaceAt(&sAppSans, false, sAppSans.file);
    +    writeFaceAt(&sAppOnly, false, sAppOnly.file);
    +    writeFaceAt(&sAppBold, false, sAppBold.file);
    +    writeFaceAt(&sMemFace, false, sMemFace.file);
    +    dvxMemAppIdPtr = &app;
    +
    +    // Before it adds any, the program shares everyone's fonts
    +    BitmapFontT *shared = dvxFontOpen("Test Sans", 12, 0);
    +
    +    TEST_TRUE(shared->glyphs['C'].width > 0);
    +    TEST_TRUE(dvxFontAddFile(TEXT_APP_ID, sAppSans.file));
    +    TEST_FALSE(dvxFontAddFile(TEXT_APP_ID, sAppSans.file));
    +    TEST_FALSE(dvxFontAddFile(TEXT_APP_ID, "NOSUCH.DFN"));
    +    TEST_FALSE(dvxFontAddFile(TEXT_APP_ID, DVX_FONTS_DIR DVX_PATH_SEP "SYSTEM.DFN"));
    +    TEST_FALSE(dvxFontAddFile(TEXT_NO_APP_ID, sAppSans.file));
    +
    +    // Its size of the family: its own face (a 'Z', no 'C')
    +    BitmapFontT *mine = dvxFontOpen("Test Sans", 12, 0);
    +
    +    TEST_TRUE(mine != shared);
    +    TEST_TRUE(mine->glyphs['Z'].width > 0);
    +    TEST_EQ(mine->glyphs['C'].width, 0);
    +    TEST_STR_EQ(dvxFontFamilyOf(mine), "Test Sans");
    +
    +    // A size it lacks from the installed face, one only it has from its own
    +    BitmapFontT *small = dvxFontOpen("Test Sans", 8, 0);
    +    BitmapFontT *big   = dvxFontOpen("Test Sans", 36, 0);
    +
    +    TEST_EQ(small->pixelSize, 8);
    +    TEST_TRUE(small->glyphs['C'].width > 0);
    +    TEST_EQ(big->pixelSize, 40);
    +    TEST_TRUE(big->glyphs['Z'].width > 0);
    +    TEST_EQ(dvxFontSizes("Test Sans", 0, sizes, TEXT_SIZES_MAX), 3);
    +    TEST_EQ(sizes[0], 8);
    +    TEST_EQ(sizes[1], 12);
    +    TEST_EQ(sizes[2], 40);
    +    TEST_EQ(FontNearestSize("Test Sans", 30, 0), 40);
    +
    +    // A style it lacks: the installed face of it, not one made from its own
    +    BitmapFontT *bold = dvxFontOpen("Test Sans", 12, FONT_STYLE_BOLD);
    +
    +    TEST_FALSE(bold->flags & FONT_FLAG_SYNTH_BOLD);
    +    TEST_TRUE(bold->glyphs['B'].width > 0);
    +    TEST_EQ(dvxFontSizes("Test Sans", FONT_STYLE_BOLD, sizes, TEXT_SIZES_MAX), 1);
    +
    +    // Smoothed as the face it comes from has the size: the installed 12
    +    // is, the program's is not
    +    dvxFontSetSmooth(true);
    +    TEST_FALSE(mine->flags & FONT_FLAG_GRAY);
    +    dvxMemAppIdPtr = &other;
    +
    +    BitmapFontT *theirs = dvxFontOpen("Test Sans", 12, 0);
    +
    +    TEST_TRUE(theirs == shared);
    +    TEST_TRUE(theirs->flags & FONT_FLAG_GRAY);
    +    dvxFontClose(theirs);
    +    dvxFontSetSmooth(false);
    +
    +    // Another program sees the installed family alone
    +    TEST_EQ(dvxFontSizes("Test Sans", 0, sizes, TEXT_SIZES_MAX), 2);
    +    TEST_EQ(dvxFontFamilyCount(), 6);
    +    dvxMemAppIdPtr = &app;
    +
    +    // A family only the program has is in its list alone; its licence
    +    TEST_TRUE(dvxFontAddFile(TEXT_APP_ID, sAppOnly.file));
    +    TEST_EQ(dvxFontFamilyCount(), 7);
    +    TEST_STR_EQ(dvxFontFamilyName(1), "App Only");
    +    TEST_STR_EQ(FontFamilyName(1), "App Only");
    +    dvxMemAppIdPtr = &other;
    +    TEST_EQ(dvxFontFamilyCount(), 6);
    +    TEST_STR_EQ(dvxFontFamilyName(1), "Bad Strike");
    +    dvxMemAppIdPtr = &app;
    +
    +    char *text = dvxFontLicence(mine);
    +
    +    TEST_TRUE(text && strstr(text, "APPSANS.DFN notice\r\n\r\nTest Sans licence\r\n"));
    +    free(text);
    +    TEST_TRUE(strstr(FontLicence("Test Sans", 8, 0), "TSANS.DFN notice") != NULL);
    +    TEST_STR_EQ(FontLicence(FONT_SYSTEM_FAMILY, FONT_CHAR_HEIGHT, 0), "");
    +    TEST_PTR_NULL(dvxFontLicence(NULL));
    +
    +    // A program file's font resources (its other resources passed over)
    +    uint8_t *boldDfn = readAll(sAppBold.file, &len);
    +
    +    writeFile(TEXT_PROGRAM, TEXT_PROGRAM_CODE, strlen(TEXT_PROGRAM_CODE));
    +    TEST_EQ(dvxResAppend(TEXT_PROGRAM, "author", DVX_RES_TEXT, "Test", (uint32_t)strlen("Test") + 1), 0);
    +    TEST_EQ(dvxResAppend(TEXT_PROGRAM, "bold", DVX_RES_FONT, boldDfn, (uint32_t)len), 0);
    +    free(boldDfn);
    +    TEST_EQ(dvxFontAddResources(TEXT_APP_ID, TEXT_PROGRAM), 1);
    +    TEST_EQ(FontAddResources(TEXT_PROGRAM), 0);
    +    TEST_EQ(dvxFontAddResources(TEXT_APP_ID, "NOSUCH.APP"), 0);
    +
    +    BitmapFontT *resBold = dvxFontOpen("App Only", 10, FONT_STYLE_BOLD);
    +
    +    TEST_FALSE(resBold->flags & FONT_FLAG_SYNTH_BOLD);
    +    text = dvxFontLicence(resBold);
    +    TEST_TRUE(text && strstr(text, "APPB.DFN notice") != NULL);
    +    free(text);
    +    dvxFontClose(resBold);
    +
    +    // From memory: a copy, so the bytes can go at once
    +    uint8_t *memDfn = readAll(sMemFace.file, &len);
    +
    +    TEST_TRUE(dvxFontAddMemory(TEXT_APP_ID, memDfn, (uint32_t)len, "memory font"));
    +    memset(memDfn, 0, (size_t)len);
    +    free(memDfn);
    +    TEST_FALSE(dvxFontAddMemory(TEXT_APP_ID, NULL, 0, "nothing"));
    +
    +    BitmapFontT *mem = dvxFontOpen("Mem Font", 9, 0);
    +
    +    TEST_TRUE(mem->glyphs['M'].width > 0);
    +    dvxFontClose(mem);
    +
    +    // The BASIC entry points add for the running program
    +    TEST_EQ(FontAddFile("NOSUCH.DFN"), 0);
    +    TEST_EQ(FontAddData("bad", (const uint8_t *)"x", 1), 0);
    +    TEST_EQ(FontAddFile(sAppOnly.file), 0);
    +
    +    // The program ends: its families go, its fonts no one holds unload, a
    +    // held one still draws
    +    dvxFontClose(small);
    +    dvxFontClose(big);
    +    dvxFontClose(bold);
    +    dvxFontRemoveOwner(TEXT_APP_ID);
    +    TEST_EQ(dvxFontFamilyCount(), 6);
    +
    +    BitmapFontT *after = dvxFontOpen("Test Sans", 12, 0);
    +
    +    TEST_TRUE(after == shared);
    +    TEST_TRUE(mine->glyphs['Z'].width > 0);
    +    dvxFontClose(after);
    +    dvxFontClose(mine);
    +
    +    // Added for every program: everyone sees it, a rescan keeps it, and
    +    // removing them leaves the installed fonts
    +    TEST_TRUE(dvxFontAddFile(0, sAppOnly.file));
    +    dvxMemAppIdPtr = &other;
    +    TEST_EQ(dvxFontFamilyCount(), 7);
    +    dvxFontRescan();
    +    TEST_EQ(dvxFontFamilyCount(), 7);
    +    dvxFontRemoveOwner(0);
    +    TEST_EQ(dvxFontFamilyCount(), 6);
    +    TEST_STR_EQ(dvxFontFamilyName(5), "Test Sans");
    +    dvxFontClose(shared);
    +    dvxMemAppIdPtr = prev;
    +}
    +
    +
    +// The BASIC entry points read the same families and fonts
    +static void testBasic(void) {
    +    TEST_EQ(FontFamilyCount(), dvxFontFamilyCount());
    +    TEST_STR_EQ(FontFamilyName(2), "DejaVu Sans");
    +    TEST_STR_EQ(FontFamilyName(99), "");
    +    TEST_EQ(FontFamilyFixed("Test Mono"), -1);
    +    TEST_EQ(FontFamilyFixed("Test Sans"), 0);
    +    TEST_EQ(FontSizeCount("Test Sans", 0), 2);
    +    TEST_EQ(FontSizeAt("Test Sans", 0, 1), 12);
    +    TEST_EQ(FontSizeAt("Test Sans", 0, 2), 0);
    +    TEST_EQ(FontSizeAt("Test Sans", 0, -1), 0);
    +    TEST_EQ(FontNearestSize("Test Sans", 11, 0), 12);
    +    TEST_EQ(FontNearestSize("Nope", 11, FONT_STYLE_BOLD), FONT_CHAR_HEIGHT);
    +    TEST_EQ(FontLineHeight("Test Sans", 12, 0), 12);
    +    TEST_EQ(FontAscent("Test Sans", 12, 0), 12 * TEXT_ASCENT_NUM / TEXT_ASCENT_DEN);
    +
    +    BitmapFontT *f = dvxFontOpen("Test Sans", 12, 0);
    +
    +    TEST_EQ(FontTextWidth("Test Sans", 12, 0, "AB"), textWidth(f, "AB"));
    +    TEST_EQ(FontTextWidth("System", 16, 0, "AB"), 2 * FONT_CHAR_WIDTH);
    +    dvxFontClose(f);
    +    FontRescan();
    +    TEST_EQ(FontFamilyCount(), 6);
    +}
    +
    +
    +// The same font opened twice is one font; closing it twice gives it up;
    +// the program running keeps its memory id
    +static void testCache(void) {
    +    int32_t  appId = TEXT_APP_ID;
    +    int32_t *prev  = dvxMemAppIdPtr;
    +
    +    dvxMemAppIdPtr = &appId;
    +
    +    BitmapFontT *a = dvxFontOpen("Test Sans", 12, 0);
    +    BitmapFontT *b = dvxFontOpen("test sans", 13, 0);
    +
    +    TEST_EQ(appId, TEXT_APP_ID);
    +    TEST_PTR_NOT_NULL(a);
    +    TEST_TRUE(a == b);
    +    TEST_STR_EQ(dvxFontFamilyOf(a), "Test Sans");
    +    dvxFontClose(a);
    +    dvxFontClose(b);
    +    dvxFontClose(b);
    +
    +    // Reopened from the idle fonts
    +    BitmapFontT *c = dvxFontOpen("Test Sans", 12, 0);
    +
    +    TEST_TRUE(c == a);
    +    dvxFontClose(c);
    +    dvxMemAppIdPtr = prev;
    +
    +    // Not from the cache: ignored
    +    dvxFontClose(NULL);
    +    dvxFontClose((BitmapFontT *)&dvxFont8x16);
    +    TEST_STR_EQ(dvxFontFamilyOf(&dvxFont8x16), FONT_SYSTEM_FAMILY);
    +    TEST_STR_EQ(dvxFontFamilyOf(NULL), FONT_SYSTEM_FAMILY);
    +}
    +
    +
    +// A program's font that is the only one loaded (the end of the loaded
    +// list) unloads when the program's fonts are removed; run while the
    +// tests hold no fonts
    +static void testDetachOldest(void) {
    +    int32_t  app  = TEXT_APP_ID;
    +    int32_t *prev = dvxMemAppIdPtr;
    +
    +    writeFaceAt(&sAppSans, false, sAppSans.file);
    +    dvxMemAppIdPtr = &app;
    +    TEST_TRUE(dvxFontAddFile(TEXT_APP_ID, sAppSans.file));
    +
    +    BitmapFontT *mine = dvxFontOpen("Test Sans", 12, 0);
    +
    +    TEST_TRUE(mine->glyphs['Z'].width > 0);
    +    dvxFontClose(mine);
    +    dvxFontRemoveOwner(TEXT_APP_ID);
    +
    +    BitmapFontT *shared = dvxFontOpen("Test Sans", 12, 0);
    +
    +    TEST_TRUE(shared->glyphs['C'].width > 0);
    +    TEST_EQ(shared->glyphs['Z'].width, 0);
    +    dvxFontClose(shared);
    +    dvxMemAppIdPtr = prev;
    +}
    +
    +
    +// The family list: the system font first, the rest alphabetical; files
    +// that cannot be read, a family called "System" and a second face of a
    +// style skipped (a face whose glyphs are spoiled reads well enough to be
    +// listed); sizes and pitch per family
    +static void testFamilies(void) {
    +    int32_t sizes[TEXT_SIZES_MAX];
    +
    +    TEST_EQ(dvxFontFamilyCount(), 6);
    +    TEST_STR_EQ(dvxFontFamilyName(0), FONT_SYSTEM_FAMILY);
    +    TEST_STR_EQ(dvxFontFamilyName(1), "Bad Strike");
    +    TEST_STR_EQ(dvxFontFamilyName(2), "DejaVu Sans");
    +    TEST_STR_EQ(dvxFontFamilyName(3), "DejaVu Sans Mono");
    +    TEST_STR_EQ(dvxFontFamilyName(4), "Test Mono");
    +    TEST_STR_EQ(dvxFontFamilyName(5), "Test Sans");
    +    TEST_PTR_NULL(dvxFontFamilyName(6));
    +    TEST_PTR_NULL(dvxFontFamilyName(-1));
    +
    +    TEST_TRUE(dvxFontFamilyFixed("Test Mono"));
    +    TEST_TRUE(dvxFontFamilyFixed("test mono"));
    +    TEST_FALSE(dvxFontFamilyFixed("Test Sans"));
    +    TEST_TRUE(dvxFontFamilyFixed(FONT_SYSTEM_FAMILY));
    +    TEST_TRUE(dvxFontFamilyFixed(NULL));
    +    TEST_FALSE(dvxFontFamilyFixed("Nope"));
    +
    +    // The duplicate's 20 px never shows; the bold face has its own sizes;
    +    // italic is drawn from the regular face
    +    TEST_EQ(dvxFontSizes("Test Sans", 0, sizes, TEXT_SIZES_MAX), 2);
    +    TEST_EQ(sizes[0], 8);
    +    TEST_EQ(sizes[1], 12);
    +    TEST_EQ(dvxFontSizes("Test Sans", 0, sizes, 1), 2);
    +    TEST_EQ(dvxFontSizes("Test Sans", FONT_STYLE_BOLD, sizes, TEXT_SIZES_MAX), 1);
    +    TEST_EQ(sizes[0], 12);
    +    TEST_EQ(dvxFontSizes("Test Sans", FONT_STYLE_ITALIC | FONT_STYLE_UNDERLINE, sizes, TEXT_SIZES_MAX), 2);
    +    TEST_EQ(dvxFontSizes("Nope", 0, sizes, TEXT_SIZES_MAX), 0);
    +    TEST_EQ(dvxFontSizes(NULL, 0, sizes, TEXT_SIZES_MAX), 1);
    +    TEST_EQ(sizes[0], FONT_CHAR_HEIGHT);
    +    TEST_EQ(dvxFontSizes(FONT_SYSTEM_FAMILY, FONT_STYLE_BOLD, NULL, 0), 1);
    +}
    +
    +
    +// What a proportional face lacks: box characters drawn to fill a cell as
    +// wide as its 0, other characters from the fallback family at the same
    +// size and style, a blank as wide as a space where it has none either
    +static void testFill(void) {
    +    BitmapFontT *f  = dvxFontOpen("Test Sans", 12, 0);
    +    BitmapFontT *fb = dvxFontOpen("DejaVu Sans", 12, 0);
    +
    +    TEST_EQ(f->pixelSize, 12);
    +    TEST_EQ(f->charHeight, 12);
    +    TEST_EQ(f->ascent, 8);
    +    TEST_TRUE(f->flags & FONT_FLAG_PROPORTIONAL);
    +    TEST_EQ(f->charWidth, f->glyphs['0'].advance);
    +    TEST_EQ(f->style, 0);
    +    TEST_EQ(f->numChars, DFN_CHAR_COUNT);
    +    TEST_TRUE(glyphSame(f, 'B', fb, 'B'));
    +    TEST_TRUE(glyphSame(f, CH_SMILEY, fb, CH_SMILEY));
    +    TEST_EQ(f->glyphs['Q'].width, 0);
    +    TEST_EQ(f->glyphs['Q'].advance, f->glyphs[' '].advance);
    +    TEST_EQ(f->glyphs[CH_NBSP].advance, f->glyphs[' '].advance);
    +
    +    // The horizontal line across the cell's middle, as tall as the line
    +    const FontGlyphT *box = &f->glyphs[CH_BOX_H];
    +
    +    TEST_EQ(box->advance, f->charWidth);
    +    TEST_EQ(box->width, f->charWidth);
    +    TEST_EQ(box->height, f->charHeight);
    +    TEST_EQ(box->top, f->ascent);
    +    TEST_EQ(box->left, 0);
    +    TEST_TRUE(rowSet(f, CH_BOX_H, (f->charHeight - f->lineThick) / 2, 0, f->charWidth));
    +    TEST_FALSE(rowSet(f, CH_BOX_H, 0, 0, 1));
    +    TEST_TRUE(rowSet(f, CH_BLOCK_FULL, f->charHeight - 1, 0, f->charWidth));
    +
    +    // Lines: under the baseline, through the lower-case letters
    +    TEST_EQ(f->lineThick, 1);
    +    TEST_TRUE(f->underlineY > f->ascent - 1 && f->underlineY <= f->charHeight - f->lineThick);
    +    TEST_TRUE(f->strikeY < f->ascent && f->strikeY > 0);
    +
    +    // The 8 px size fills from the fallback's 8 px
    +    BitmapFontT *s8  = dvxFontOpen("Test Sans", 10, 0);
    +    BitmapFontT *fb8 = dvxFontOpen("DejaVu Sans", 8, 0);
    +
    +    TEST_EQ(s8->pixelSize, 8);
    +    TEST_TRUE(glyphSame(s8, 'B', fb8, 'B'));
    +    dvxFontClose(s8);
    +    dvxFontClose(fb8);
    +    dvxFontClose(f);
    +    dvxFontClose(fb);
    +}
    +
    +
    +// A fixed-pitch face: every advance its pitch, fallback glyphs centered in
    +// it, its own box characters replaced (it asks for that); made bold, a
    +// pitch one column wider
    +static void testFixed(void) {
    +    BitmapFontT *m  = dvxFontOpen("Test Mono", 10, 0);
    +    BitmapFontT *fb = dvxFontOpen("DejaVu Sans Mono", 10, 0);
    +    int32_t      pitch;
    +
    +    TEST_FALSE(m->flags & FONT_FLAG_PROPORTIONAL);
    +    pitch = m->charWidth;
    +    TEST_EQ(pitch, m->glyphs[' '].advance);
    +
    +    for (int32_t ch = 0; ch < DFN_CHAR_COUNT; ch++) {
    +        if (m->glyphs[ch].advance != pitch) {
    +            unitFail(__FILE__, __LINE__, "Test Mono character %02" PRIX32 " advances %" PRId32 ", not the pitch %" PRId32, (uint32_t)ch, (int32_t)m->glyphs[ch].advance, pitch);
    +        }
    +    }
    +
    +    TEST_EQ(m->glyphs['B'].left, fb->glyphs['B'].left + (pitch - fb->glyphs['B'].advance) / 2);
    +    TEST_TRUE(glyphSame(m, 'B', fb, 'B'));
    +    TEST_EQ(m->glyphs[CH_BOX_H].width, pitch);
    +    TEST_TRUE(rowSet(m, CH_BOX_H, (m->charHeight - m->lineThick) / 2, 0, pitch));
    +
    +    BitmapFontT *mb = dvxFontOpen("Test Mono", 10, FONT_STYLE_BOLD);
    +
    +    TEST_TRUE(mb->flags & FONT_FLAG_SYNTH_BOLD);
    +    TEST_EQ(mb->charWidth, pitch + 1);
    +    TEST_EQ(mb->glyphs['A'].advance, pitch + 1);
    +    TEST_EQ(mb->glyphs[CH_BOX_H].width, pitch + 1);
    +    TEST_EQ(mb->glyphs[CH_BOX_H].advance, pitch + 1);
    +    dvxFontClose(mb);
    +    dvxFontClose(m);
    +    dvxFontClose(fb);
    +}
    +
    +
    +void testFonts(void) {
    +    char dir[UNIT_PATH_MAX];
    +    char home[UNIT_PATH_MAX];
    +
    +    unitScratchPath(dir, sizeof(dir), TEXT_DIR);
    +    mkdir(dir, TEXT_DIR_MODE);
    +
    +    if (!getcwd(home, sizeof(home)) || chdir(dir) != 0) {
    +        unitFail(__FILE__, __LINE__, "cannot work in %s", dir);
    +        return;
    +    }
    +
    +    mkdir(DVX_FONTS_DIR, TEXT_DIR_MODE);
    +
    +    for (int32_t i = 0; i < DVX_ARRAY_LEN(sFaces); i++) {
    +        writeFace(&sFaces[i], i == FACE_BAD_STRIKE);
    +    }
    +
    +    writeFile(DVX_FONTS_DIR DVX_PATH_SEP "BROKEN.DFN", "not a font", strlen("not a font"));
    +    writeFile(DVX_FONTS_DIR DVX_PATH_SEP "README.TXT", "fonts", strlen("fonts"));
    +    dvxFontRescan();
    +
    +    testDetachOldest();
    +    testFamilies();
    +    testCache();
    +    testSystem();
    +    testFill();
    +    testFixed();
    +    testStyles();
    +    testBasic();
    +    testWidgets();
    +    testSmooth();
    +    testAdded();
    +    testNoFallback();
    +
    +    dvxFontRescan();
    +
    +    if (chdir(home) != 0) {
    +        unitFail(__FILE__, __LINE__, "cannot return to %s", home);
    +    }
    +}
    +
    +
    +// Without the fallback families, what a face lacks comes from the system
    +// font
    +static void testNoFallback(void) {
    +    remove(DVX_FONTS_DIR DVX_PATH_SEP "DJVSANS.DFN");
    +    remove(DVX_FONTS_DIR DVX_PATH_SEP "DJVSANSB.DFN");
    +    remove(DVX_FONTS_DIR DVX_PATH_SEP "DJVMONO.DFN");
    +    dvxFontRescan();
    +    TEST_EQ(dvxFontFamilyCount(), 4);
    +
    +    BitmapFontT *f = dvxFontOpen("Test Sans", 12, 0);
    +
    +    TEST_EQ(f->glyphs['B'].width, FONT_CHAR_WIDTH);
    +    TEST_EQ(f->glyphs['B'].height, FONT_CHAR_HEIGHT);
    +    TEST_EQ(f->glyphs['B'].top, FONT_SYS_ASCENT);
    +    TEST_MEM_EQ(f->glyphData + f->glyphAt['B'], font8x16 + 'B' * FONT_CHAR_HEIGHT, FONT_CHAR_HEIGHT);
    +    dvxFontClose(f);
    +
    +    // A face whose strike cannot be read draws as the system font
    +    BitmapFontT *bad = dvxFontOpen("Bad Strike", 10, 0);
    +
    +    TEST_PTR_NULL(bad->glyphs);
    +    TEST_EQ(bad->charWidth, FONT_CHAR_WIDTH);
    +    TEST_STR_EQ(dvxFontFamilyOf(bad), FONT_SYSTEM_FAMILY);
    +    dvxFontClose(bad);
    +}
    +
    +
    +// Styles: a face of the style when the family has one, else made from the
    +// nearest; bold thickens and widens, italic leans
    +// Smoothing: off at first.  On, the fonts held are built again in place
    +// from their sizes' gray strikes (the same pointer, a new serial), a
    +// made style from them too; a size or a face without one stays as it is,
    +// and a font with lines follows the font it draws.  Off again, back to
    +// the 1-bit strikes.
    +static void testSmooth(void) {
    +    BitmapFontT *f      = dvxFontOpen("Test Sans", 12, 0);
    +    BitmapFontT *under  = dvxFontOpen("Test Sans", 12, FONT_STYLE_UNDERLINE);
    +    BitmapFontT *ital   = dvxFontOpen("Test Sans", 12, FONT_STYLE_ITALIC);
    +    BitmapFontT *small  = dvxFontOpen("Test Sans", 8, 0);
    +    BitmapFontT *bold   = dvxFontOpen("Test Sans", 12, FONT_STYLE_BOLD);
    +    uint32_t     serial = f->serial;
    +    uint32_t     smallS = small->serial;
    +    uint32_t     boldS  = bold->serial;
    +    int32_t      w      = f->glyphs['A'].width;
    +
    +    TEST_FALSE(dvxFontSmooth());
    +    TEST_FALSE(f->flags & FONT_FLAG_GRAY);
    +    TEST_EQ(FONT_BPP(f), 1);
    +    TEST_TRUE(rowSet(f, 'A', 2, 0, w));
    +
    +    dvxFontSetSmooth(true);
    +    TEST_TRUE(dvxFontSmooth());
    +    TEST_TRUE(f->flags & FONT_FLAG_GRAY);
    +    TEST_EQ(FONT_BPP(f), FONT_GRAY_BPP);
    +    TEST_NE(f->serial, serial);
    +    TEST_EQ(f->glyphs['A'].width, w);
    +    TEST_EQ(levelAt(f, 'A', 0, 0), FONT_GRAY_FULL);
    +    TEST_EQ(levelAt(f, 'A', 0, 1), 0);
    +    TEST_EQ(levelAt(f, 'A', 2, 0), TEXT_GRAY_SOFT);
    +    TEST_EQ(levelAt(f, 'A', 2, 1), TEXT_GRAY_SOFT + 1);
    +    TEST_TRUE(under->flags & FONT_FLAG_GRAY);
    +    TEST_TRUE(under->style & FONT_STYLE_UNDERLINE);
    +    TEST_TRUE(under->glyphs == f->glyphs);
    +    TEST_TRUE(ital->flags & FONT_FLAG_GRAY);
    +    TEST_TRUE(ital->flags & FONT_FLAG_SYNTH_ITALIC);
    +    TEST_FALSE(small->flags & FONT_FLAG_GRAY);
    +    TEST_EQ(small->serial, smallS);
    +    TEST_FALSE(bold->flags & FONT_FLAG_GRAY);
    +    TEST_EQ(bold->serial, boldS);
    +
    +    // Opened now: the smoothed font held already
    +    BitmapFontT *again = dvxFontOpen("test sans", 12, 0);
    +
    +    TEST_TRUE(again == f);
    +    dvxFontClose(again);
    +
    +    testSmoothDraw(f, 32);
    +    testSmoothDraw(f, 16);
    +    testSmoothDraw(f, 8);
    +
    +    // Asked again: nothing changes
    +    serial = f->serial;
    +    dvxFontSetSmooth(true);
    +    TEST_EQ(f->serial, serial);
    +
    +    dvxFontSetSmooth(false);
    +    TEST_FALSE(dvxFontSmooth());
    +    TEST_FALSE(f->flags & FONT_FLAG_GRAY);
    +    TEST_FALSE(under->flags & FONT_FLAG_GRAY);
    +    TEST_TRUE(under->glyphs == f->glyphs);
    +    TEST_TRUE(rowSet(f, 'A', 2, 0, w));
    +
    +    dvxFontClose(f);
    +    dvxFontClose(under);
    +    dvxFontClose(ital);
    +    dvxFontClose(small);
    +    dvxFontClose(bold);
    +}
    +
    +
    +// A smoothed glyph drawn at bpp bits per pixel, white over black and one
    +// blue pixel: above 8 bits a partial level blends white into what is
    +// under it, at 8 only the levels of FONT_GRAY_HALF and more are drawn
    +static void testSmoothDraw(BitmapFontT *font, int32_t bpp) {
    +    DisplayT  d;
    +    BlitOpsT  ops;
    +
    +    unitDisplayCreate(&d, UNIT_SCREEN_W, UNIT_SCREEN_H, bpp);
    +    drawInit(&ops, &d);
    +
    +    uint32_t white = packColor(&d, 255, 255, 255);
    +    uint32_t black = packColor(&d, 0, 0, 0);
    +    uint32_t blue  = packColor(&d, 0, 0, 255);
    +    int32_t  x     = TEXT_GRAY_X;
    +    int32_t  y     = TEXT_GRAY_Y;
    +
    +    rectFill(&d, &ops, 0, 0, UNIT_SCREEN_W, UNIT_SCREEN_H, black);
    +    unitPixelSet(&d, x + 2, y + 2, blue);
    +    drawText(&d, &ops, font, x, y, "A", white, black, false);
    +
    +    TEST_EQ(unitPixelGet(&d, x, y), white);
    +    TEST_EQ(unitPixelGet(&d, x + 1, y), black);
    +
    +    if (bpp == 8) {
    +        TEST_EQ(unitPixelGet(&d, x, y + 2), black);
    +        TEST_EQ(unitPixelGet(&d, x + 1, y + 2), white);
    +        TEST_EQ(unitPixelGet(&d, x + 2, y + 2), blue);
    +    } else {
    +        // A third and two thirds of the way to white
    +        TEST_EQ(unitPixelGet(&d, x, y + 2), packColor(&d, 85, 85, 85));
    +        TEST_EQ(unitPixelGet(&d, x + 2, y + 2), packColor(&d, 85, 85, 255));
    +    }
    +
    +    if (bpp == 32) {
    +        TEST_EQ(unitPixelGet(&d, x + 1, y + 2), packColor(&d, 170, 170, 170));
    +    }
    +
    +    unitDisplayDestroy(&d);
    +}
    +
    +
    +static void testStyles(void) {
    +    BitmapFontT *r  = dvxFontOpen("Test Sans", 12, 0);
    +    BitmapFontT *b  = dvxFontOpen("Test Sans", 8, FONT_STYLE_BOLD);
    +    BitmapFontT *i  = dvxFontOpen("Test Sans", 12, FONT_STYLE_ITALIC | FONT_STYLE_STRIKE);
    +    BitmapFontT *bi = dvxFontOpen("Test Sans", 12, FONT_STYLE_BOLD | FONT_STYLE_ITALIC);
    +    BitmapFontT *fb = dvxFontOpen("DejaVu Sans", 12, FONT_STYLE_BOLD);
    +
    +    // The bold face's own glyphs, at its only size; the fallback's bold
    +    TEST_EQ(b->pixelSize, 12);
    +    TEST_EQ(b->style, FONT_STYLE_BOLD);
    +    TEST_FALSE(b->flags & FONT_FLAG_SYNTH_BOLD);
    +    TEST_FALSE(glyphSame(b, 'A', r, 'A'));
    +    TEST_TRUE(glyphSame(b, 'Q', fb, 'Q'));
    +
    +    // Italic made from the regular face: wider, leaning right above the
    +    // baseline; same advances.  Struck through, it is the italic font's
    +    // glyphs with the line in its style.
    +    BitmapFontT *plain = dvxFontOpen("Test Sans", 12, FONT_STYLE_ITALIC);
    +
    +    TEST_EQ(i->style, FONT_STYLE_ITALIC | FONT_STYLE_STRIKE);
    +    TEST_EQ(plain->style, FONT_STYLE_ITALIC);
    +    TEST_TRUE(i->glyphs == plain->glyphs);
    +    TEST_TRUE(i->glyphData == plain->glyphData);
    +    TEST_STR_EQ(dvxFontFamilyOf(i), "Test Sans");
    +    dvxFontClose(plain);
    +    TEST_TRUE(i->flags & FONT_FLAG_SYNTH_ITALIC);
    +    TEST_TRUE(i->glyphs['A'].width > r->glyphs['A'].width);
    +    TEST_EQ(i->glyphs['A'].advance, r->glyphs['A'].advance);
    +    TEST_EQ(i->glyphs[CH_BOX_H].width, r->glyphs[CH_BOX_H].width);
    +
    +    // Bold italic: the bold face, slanted
    +    TEST_TRUE(bi->flags & FONT_FLAG_SYNTH_ITALIC);
    +    TEST_FALSE(bi->flags & FONT_FLAG_SYNTH_BOLD);
    +    TEST_TRUE(bi->glyphs['B'].width > b->glyphs['B'].width);
    +
    +    dvxFontClose(r);
    +    dvxFontClose(b);
    +    dvxFontClose(i);
    +    dvxFontClose(bi);
    +    dvxFontClose(fb);
    +}
    +
    +
    +// The system font: plain, it is the 8x16 font itself (the fast paths);
    +// made bold or italic, a loaded font with its box characters still joining
    +static void testSystem(void) {
    +    BitmapFontT *s = dvxFontOpen(NULL, 12, 0);
    +
    +    TEST_PTR_NULL(s->glyphs);
    +    TEST_TRUE(s->glyphData == font8x16);
    +    TEST_EQ(s->charWidth, FONT_CHAR_WIDTH);
    +    TEST_EQ(s->pixelSize, FONT_CHAR_HEIGHT);
    +    TEST_TRUE(s == dvxFontOpen("Nope", 8, 0));
    +    TEST_STR_EQ(dvxFontFamilyOf(s), FONT_SYSTEM_FAMILY);
    +
    +    // Underlined: the same cells (still the fast paths), the line in its
    +    // style
    +    BitmapFontT *u = dvxFontOpen(FONT_SYSTEM_FAMILY, 30, FONT_STYLE_UNDERLINE);
    +
    +    TEST_TRUE(u != s);
    +    TEST_EQ(u->style, FONT_STYLE_UNDERLINE);
    +    TEST_PTR_NULL(u->glyphs);
    +    TEST_TRUE(u->glyphData == font8x16);
    +    TEST_TRUE(u == dvxFontOpen(NULL, 16, FONT_STYLE_UNDERLINE));
    +    dvxFontClose(u);
    +    dvxFontClose(u);
    +    dvxFontClose(s);
    +    dvxFontClose(s);
    +
    +    BitmapFontT *b = dvxFontOpen(FONT_SYSTEM_FAMILY, 16, FONT_STYLE_BOLD);
    +
    +    TEST_PTR_NOT_NULL(b->glyphs);
    +    TEST_TRUE(b->flags & FONT_FLAG_SYNTH_BOLD);
    +    TEST_EQ(b->charWidth, FONT_CHAR_WIDTH + 1);
    +    TEST_EQ(b->glyphs['A'].advance, FONT_CHAR_WIDTH + 1);
    +    TEST_EQ(b->glyphs['A'].width, FONT_CHAR_WIDTH + 1);
    +    TEST_EQ(b->glyphs[CH_BOX_H].width, FONT_CHAR_WIDTH + 1);
    +    TEST_TRUE(rowSet(b, CH_BOX_H, (FONT_CHAR_HEIGHT - 1) / 2, 0, FONT_CHAR_WIDTH + 1));
    +    TEST_EQ(b->ascent, FONT_SYS_ASCENT);
    +    TEST_EQ(textWidth(b, "AB"), 2 * (FONT_CHAR_WIDTH + 1));
    +    dvxFontClose(b);
    +
    +    BitmapFontT *i = dvxFontOpen("Nope", 20, FONT_STYLE_ITALIC);
    +
    +    TEST_TRUE(i->flags & FONT_FLAG_SYNTH_ITALIC);
    +    TEST_EQ(i->charWidth, FONT_CHAR_WIDTH);
    +    TEST_EQ(i->glyphs['A'].advance, FONT_CHAR_WIDTH);
    +    TEST_TRUE(i->glyphs['A'].width > FONT_CHAR_WIDTH);
    +    TEST_MEM_EQ(i->glyphData + i->glyphAt[CH_BOX_H], font8x16 + CH_BOX_H * FONT_CHAR_HEIGHT, FONT_CHAR_HEIGHT);
    +    dvxFontClose(i);
    +}
    +
    +
    +// Widget fonts: a widget's own, else its nearest container's, else the
    +// one passed down; a class that edits by columns refuses a proportional
    +// font; the class dispatch hands each method its widget's font
    +static void testWidgets(void) {
    +    static WidgetClassT plainClass;
    +    static WidgetClassT fixedClass;
    +    WidgetT             root;
    +    WidgetT             box;
    +    WidgetT             leaf;
    +    WidgetT             editor;
    +
    +    memset(&plainClass, 0, sizeof(plainClass));
    +    memset(&fixedClass, 0, sizeof(fixedClass));
    +    memset(&root, 0, sizeof(root));
    +    memset(&box, 0, sizeof(box));
    +    memset(&leaf, 0, sizeof(leaf));
    +    memset(&editor, 0, sizeof(editor));
    +    fixedClass.flags                       = WCLASS_FIXED_FONT;
    +    plainClass.handlers[WGT_METHOD_PAINT]  = (void *)recordPaint;
    +    root.wclass                            = &plainClass;
    +    box.wclass                             = &plainClass;
    +    leaf.wclass                            = &plainClass;
    +    editor.wclass                          = &fixedClass;
    +    box.parent                             = &root;
    +    leaf.parent                            = &box;
    +    editor.parent                          = &box;
    +
    +    // Nothing set: the passed font, NULL included (with no window,
    +    // wgtFont's is the system font)
    +    TEST_TRUE(wgtFontFor(&leaf, &dvxFont8x16) == &dvxFont8x16);
    +    TEST_PTR_NULL(wgtFontFor(&editor, NULL));
    +    TEST_TRUE(wgtFont(&leaf) == &dvxFont8x16);
    +
    +    // The root's font reaches the leaf; the editor keeps the system font
    +    // in place of a proportional one
    +    TEST_TRUE(wgtSetFont(&root, "Test Sans", 12, 0));
    +    TEST_TRUE(wgtFontFor(&leaf, &dvxFont8x16) == root.font);
    +    TEST_TRUE(wgtFontFor(&editor, &dvxFont8x16) == &dvxFont8x16);
    +
    +    // No widget: the inherited font as it is (a proportional one too), and
    +    // wgtFont's system font
    +    TEST_TRUE(wgtFontFor(NULL, root.font) == root.font);
    +    TEST_TRUE(wgtFont(NULL) == &dvxFont8x16);
    +
    +    // A nearer font wins; a fixed-pitch one reaches the editor too
    +    TEST_TRUE(wgtSetFont(&box, "Test Mono", 10, FONT_STYLE_BOLD));
    +    TEST_TRUE(wgtFontFor(&leaf, &dvxFont8x16) == box.font);
    +    TEST_TRUE(wgtFontFor(&editor, &dvxFont8x16) == box.font);
    +    TEST_STR_EQ(dvxFontFamilyOf(wgtFont(&editor)), "Test Mono");
    +
    +    // The dispatch paints the leaf with its effective font
    +    sPaintedWith = NULL;
    +    wclsPaint(&leaf, NULL, NULL, &dvxFont8x16, NULL);
    +    TEST_TRUE(sPaintedWith == box.font);
    +
    +    // Back to inheriting: the root's again
    +    TEST_TRUE(wgtSetFont(&box, NULL, 0, 0));
    +    TEST_PTR_NULL(box.font);
    +    TEST_TRUE(wgtFontFor(&leaf, &dvxFont8x16) == root.font);
    +    TEST_FALSE(wgtSetFont(NULL, "Test Sans", 12, 0));
    +
    +    TEST_TRUE(wgtSetFont(&root, NULL, 0, 0));
    +}
    +
    +
    +// FONTS\file> (writeFaceAt)
    +static void writeFace(const FaceDefT *def, bool spoil) {
    +    char path[UNIT_PATH_MAX];
    +
    +    snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", DVX_FONTS_DIR, def->file);
    +    writeFaceAt(def, spoil, path);
    +}
    +
    +
    +// The face at path: each of its sizes, each character it has a glyph of
    +// its own (spoil: glyph sizes that do not add up to the strike's
    +// bitmaps), then its notice (" notice") and licence ("
    +// licence") texts
    +static void writeFaceAt(const FaceDefT *def, bool spoil, const char *path) {
    +    DfnHeaderT h;
    +    DfnStrikeT strikes[TEXT_STRIKES_MAX];
    +    FontGlyphT glyphs[TEXT_STRIKES_MAX][DFN_CHAR_COUNT];
    +    uint8_t    bits[TEXT_STRIKES_MAX][TEXT_BITS_MAX];
    +    char       notice[TEXT_TEXT_MAX];
    +    char       licence[TEXT_TEXT_MAX];
    +    int32_t    count = 0;
    +
    +    memset(&h, 0, sizeof(h));
    +    memset(strikes, 0, sizeof(strikes));
    +    memset(glyphs, 0, sizeof(glyphs));
    +    memset(bits, 0, sizeof(bits));
    +
    +    // Each size's 1-bit strike, its smoothed one after it
    +    for (int32_t s = 0; s < def->sizeCount; s++) {
    +        strikeMake(def, def->sizes[s], false, &strikes[count], glyphs[count], bits[count]);
    +        count++;
    +
    +        if (def->smoothMask & (1u << s)) {
    +            strikeMake(def, def->sizes[s], true, &strikes[count], glyphs[count], bits[count]);
    +            count++;
    +        }
    +    }
    +
    +    uint32_t offset = (uint32_t)(sizeof(DfnHeaderT) + sizeof(DfnStrikeT) * (size_t)count);
    +
    +    for (int32_t s = 0; s < count; s++) {
    +        if (spoil) {
    +            glyphs[s]['A'].height++;
    +        }
    +
    +        strikes[s].glyphOffset   = offset;
    +        offset                  += (uint32_t)sizeof(glyphs[s]);
    +        strikes[s].bitmapOffset  = offset;
    +        offset                  += strikes[s].bitmapBytes;
    +    }
    +
    +    snprintf(notice, sizeof(notice), "%s notice\r\n", def->file);
    +    snprintf(licence, sizeof(licence), "%s licence\r\n", def->family);
    +    memcpy(h.magic, DFN_MAGIC, DFN_MAGIC_LEN);
    +    h.version       = DFN_VERSION;
    +    h.style         = def->style;
    +    h.faceFlags     = def->faceFlags;
    +    h.strikeCount   = (uint16_t)count;
    +    h.noticeOffset  = offset;
    +    h.noticeBytes   = (uint32_t)strlen(notice);
    +    h.licenceOffset = offset + h.noticeBytes;
    +    h.licenceBytes  = (uint32_t)strlen(licence);
    +    snprintf(h.family, sizeof(h.family), "%s", def->family);
    +
    +    FILE *fp = fopen(path, "wb");
    +
    +    if (!fp) {
    +        unitFail(__FILE__, __LINE__, "cannot write %s", path);
    +        return;
    +    }
    +
    +    fwrite(&h, sizeof(h), 1, fp);
    +    fwrite(strikes, sizeof(DfnStrikeT), (size_t)count, fp);
    +
    +    for (int32_t s = 0; s < count; s++) {
    +        fwrite(glyphs[s], sizeof(glyphs[s]), 1, fp);
    +        fwrite(bits[s], 1, strikes[s].bitmapBytes, fp);
    +    }
    +
    +    fputs(notice, fp);
    +    fputs(licence, fp);
    +    fclose(fp);
    +}
    +
    +
    +static void writeFile(const char *name, const void *data, size_t len) {
    +    FILE *fp = fopen(name, "wb");
    +
    +    if (fp) {
    +        fwrite(data, 1, len, fp);
    +        fclose(fp);
    +    }
    +}
    diff --git a/src/test/unit/testRichText.c b/src/test/unit/testRichText.c
    new file mode 100644
    index 00000000..8c2b886b
    --- /dev/null
    +++ b/src/test/unit/testRichText.c
    @@ -0,0 +1,292 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// testRichText.c  -- unit section "richtext": styled text and RTF
    +// (src/libs/kpunch/richtext)
    +//
    +// The model's edits are checked the way undo uses them: a piece copied
    +// out, deleted and put back must give the text it came from exactly --
    +// characters, runs, styles and alignments.  RTF is checked by a round
    +// trip and by reading a document as a common word processor writes it.
    +
    +#include "unitTest.h"
    +#include "dvxFontFmt.h"
    +#include "../../libs/kpunch/richtext/richText.h"
    +#include "stb_ds_wrap.h"
    +
    +#include 
    +#include 
    +
    +#define RT_PLAIN          "Hello\tworld\r\nline2"
    +#define RT_EXPANDED       "Hello    world\nline2"
    +#define RT_HELLO_LEN      5
    +#define RT_NEWLINE_AT     14
    +#define RT_CUT_FROM       3
    +#define RT_CUT_TO         17
    +#define RT_SERIF          "DejaVu Serif"
    +#define RT_SANS           "DejaVu Sans"
    +#define RT_SERIF_SIZE     20
    +#define RT_RED            0x00FF0000u
    +#define RT_E_ACUTE        0x82       // code page 437
    +#define RT_BOX_H          0xC4
    +#define RT_WORDPAD_SIZE   15         // \fs22 in pixels
    +#define RT_WORDPAD_BIG    21         // \fs32
    +
    +// What richRtfRead gives for this, as a common word processor writes it
    +static const char sWordPad[] =
    +    "{\\rtf1\\ansi\\ansicpg1252\\deff0\\nouicompat\\deflang1033{\\fonttbl{\\f0\\fnil\\fcharset0 Calibri;}{\\f1\\fnil Times New Roman;}}\r\n"
    +    "{\\colortbl ;\\red255\\green0\\blue0;}\r\n"
    +    "{\\*\\generator Riched20 10.0.19041}\\viewkind4\\uc1 \r\n"
    +    "\\pard\\sa200\\sl276\\slmult1\\f0\\fs22\\lang9 Hello \\b bold\\b0  and \\cf1 red\\cf0 .\\par\r\n"
    +    "\\pard\\qc\\f1\\fs32 Caf\\'e9 \\u8364?\\par\r\n"
    +    "}\r\n";
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +const char  *RtfToText(const char *rtf);
    +static bool  sameText(const RichTextT *a, const RichTextT *b);
    +static void  testModel(void);
    +static void  testPieces(void);
    +void         testRichText(void);
    +static void  testRtf(void);
    +static void  testRtfForeign(void);
    +const char  *TextToRtf(const char *text);
    +
    +
    +// The same characters, runs (by style, not index), and alignments
    +static bool sameText(const RichTextT *a, const RichTextT *b) {
    +    if (richTextLength(a) != richTextLength(b) || arrlen(a->runs) != arrlen(b->runs) || arrlen(a->aligns) != arrlen(b->aligns)) {
    +        return false;
    +    }
    +
    +    if ((richTextLength(a) > 0 && memcmp(a->text, b->text, (size_t)richTextLength(a)) != 0) || memcmp(a->aligns, b->aligns, sizeof(int32_t) * (size_t)arrlen(a->aligns)) != 0) {
    +        return false;
    +    }
    +
    +    for (int32_t i = 0; i < arrlen(a->runs); i++) {
    +        const RichStyleT *sa = &a->styles[a->runs[i].style];
    +        const RichStyleT *sb = &b->styles[b->runs[i].style];
    +
    +        if (a->runs[i].start != b->runs[i].start || sa->size != sb->size || sa->style != sb->style || sa->color != sb->color || strcmp(sa->family, sb->family) != 0) {
    +            return false;
    +        }
    +    }
    +
    +    return true;
    +}
    +
    +
    +// An empty text; plain text in (tabs and returns tidied); paragraphs;
    +// restyling (one style per look); a run's style at a place
    +static void testModel(void) {
    +    RichTextT   rt;
    +    RichChangeT bold;
    +
    +    richTextInit(&rt);
    +    TEST_EQ(richTextLength(&rt), 0);
    +    TEST_EQ(richTextParaCount(&rt), 1);
    +    TEST_EQ(arrlen(rt.runs), 1);
    +    TEST_EQ(richTextStyleAt(&rt, 0), 0);
    +
    +    TEST_EQ(richTextInsertPlain(&rt, 0, RT_PLAIN, (int32_t)strlen(RT_PLAIN), 0), (int32_t)strlen(RT_EXPANDED));
    +    TEST_TRUE(memcmp(rt.text, RT_EXPANDED, strlen(RT_EXPANDED)) == 0);
    +    TEST_EQ(richTextParaCount(&rt), 2);
    +    TEST_EQ(richTextParaEnd(&rt, 0), RT_NEWLINE_AT);
    +    TEST_EQ(richTextParaStart(&rt, 1), RT_NEWLINE_AT + 1);
    +    TEST_EQ(richTextParaEnd(&rt, 1), richTextLength(&rt));
    +    TEST_EQ(richTextParaOf(&rt, RT_NEWLINE_AT), 0);
    +    TEST_EQ(richTextParaOf(&rt, RT_NEWLINE_AT + 1), 1);
    +
    +    memset(&bold, 0, sizeof(bold));
    +    bold.styleMask = FONT_STYLE_BOLD;
    +    bold.to.style  = FONT_STYLE_BOLD;
    +    richTextRestyle(&rt, 0, RT_HELLO_LEN, &bold);
    +    TEST_EQ(arrlen(rt.runs), 2);
    +    TEST_EQ(rt.runs[1].start, RT_HELLO_LEN);
    +    TEST_TRUE(rt.styles[richTextStyleAt(&rt, 0)].style & FONT_STYLE_BOLD);
    +    TEST_EQ(richTextStyleAt(&rt, RT_HELLO_LEN), 0);
    +
    +    // The same look again is the same style; restyling it all bold makes
    +    // one run
    +    richTextRestyle(&rt, 0, 2, &bold);
    +    TEST_EQ(arrlen(rt.styles), 2);
    +    richTextRestyle(&rt, 0, richTextLength(&rt), &bold);
    +    TEST_EQ(arrlen(rt.runs), 1);
    +
    +    // Inserting in a style of its own; all gone leaves one run
    +    richTextInsertPlain(&rt, 0, "x", 1, 0);
    +    TEST_EQ(arrlen(rt.runs), 2);
    +    richTextReplace(&rt, 0, richTextLength(&rt), NULL);
    +    TEST_EQ(richTextLength(&rt), 0);
    +    TEST_EQ(arrlen(rt.runs), 1);
    +    TEST_EQ(richTextParaCount(&rt), 1);
    +
    +    // Plain text as the whole, in a style that becomes the default
    +    RichStyleT serif;
    +
    +    memset(&serif, 0, sizeof(serif));
    +    strcpy(serif.family, RT_SERIF);
    +    serif.color = RICH_COLOR_DEFAULT;
    +    richTextFromPlain(&rt, "a\nb", 3, &serif);
    +    TEST_EQ(richTextParaCount(&rt), 2);
    +    TEST_STR_EQ(rt.styles[0].family, RT_SERIF);
    +    richTextFree(&rt);
    +}
    +
    +
    +// A piece copied out, deleted and put back is the text it was; joined
    +// paragraphs keep the first one's alignment, and the piece brings back
    +// the second one's
    +static void testPieces(void) {
    +    RichTextT   rt;
    +    RichTextT   before;
    +    RichTextT   piece;
    +    RichChangeT bold;
    +
    +    richTextInit(&rt);
    +    richTextInit(&before);
    +    richTextInit(&piece);
    +    richTextInsertPlain(&rt, 0, RT_PLAIN, (int32_t)strlen(RT_PLAIN), 0);
    +    memset(&bold, 0, sizeof(bold));
    +    bold.styleMask = FONT_STYLE_BOLD;
    +    bold.to.style  = FONT_STYLE_BOLD;
    +    richTextRestyle(&rt, 0, RT_HELLO_LEN, &bold);
    +    richTextSetAlign(&rt, 1, 1, FLOW_ALIGN_CENTER);
    +    richTextCopy(&rt, 0, richTextLength(&rt), &before);
    +    TEST_TRUE(sameText(&rt, &before));
    +
    +    richTextCopy(&rt, RT_CUT_FROM, RT_CUT_TO, &piece);
    +    TEST_EQ(richTextLength(&piece), RT_CUT_TO - RT_CUT_FROM);
    +    TEST_EQ(richTextParaCount(&piece), 2);
    +    TEST_EQ(piece.aligns[1], FLOW_ALIGN_CENTER);
    +    TEST_EQ(arrlen(piece.runs), 2);
    +
    +    richTextReplace(&rt, RT_CUT_FROM, RT_CUT_TO, NULL);
    +    TEST_EQ(richTextParaCount(&rt), 1);
    +    TEST_EQ(rt.aligns[0], FLOW_ALIGN_LEFT);
    +    TEST_EQ(richTextReplace(&rt, RT_CUT_FROM, RT_CUT_FROM, &piece), RT_CUT_TO - RT_CUT_FROM);
    +    TEST_TRUE(sameText(&rt, &before));
    +
    +    // An empty copy carries the style where it was taken
    +    richTextCopy(&rt, 1, 1, &piece);
    +    TEST_EQ(richTextLength(&piece), 0);
    +    TEST_TRUE(piece.styles[piece.runs[0].style].style & FONT_STYLE_BOLD);
    +
    +    richTextFree(&rt);
    +    richTextFree(&before);
    +    richTextFree(&piece);
    +}
    +
    +
    +void testRichText(void) {
    +    testModel();
    +    testPieces();
    +    testRtf();
    +    testRtfForeign();
    +}
    +
    +
    +// Written and read back: the same text -- the default family and size
    +// stay the default, characters beyond ASCII and RTF's own come back
    +static void testRtf(void) {
    +    RichTextT   rt;
    +    RichTextT   back;
    +    RichChangeT change;
    +    char        text[] = { 'a', '{', '\\', '}', (char)RT_E_ACUTE, (char)RT_BOX_H, '\n', 'b', 'c', '\n', '\0' };
    +
    +    richTextInit(&rt);
    +    richTextInit(&back);
    +    richTextInsertPlain(&rt, 0, text, (int32_t)strlen(text), 0);
    +    memset(&change, 0, sizeof(change));
    +    change.set       = RICH_SET_FAMILY | RICH_SET_SIZE | RICH_SET_COLOR;
    +    change.styleMask = FONT_STYLE_ITALIC | FONT_STYLE_UNDERLINE;
    +    change.to.style  = FONT_STYLE_ITALIC | FONT_STYLE_UNDERLINE;
    +    change.to.size   = RT_SERIF_SIZE;
    +    change.to.color  = RT_RED;
    +    strcpy(change.to.family, RT_SERIF);
    +    richTextRestyle(&rt, 7, 8, &change);
    +    richTextSetAlign(&rt, 1, 1, FLOW_ALIGN_RIGHT);
    +    richTextSetAlign(&rt, 2, 2, FLOW_ALIGN_CENTER);
    +
    +    int32_t len = 0;
    +    char   *rtf = richRtfWrite(&rt, RT_SANS, &len);
    +
    +    TEST_TRUE(rtf != NULL);
    +    TEST_EQ(len, (int32_t)strlen(rtf));
    +    TEST_TRUE(richRtfIs(rtf, len));
    +    TEST_TRUE(strstr(rtf, "\\f0 " RT_SANS ";") != NULL);
    +    TEST_TRUE(richRtfRead(&back, rtf, len));
    +    TEST_TRUE(sameText(&rt, &back));
    +    free(rtf);
    +
    +    // An empty text, and text that is not RTF
    +    richTextClear(&rt);
    +    rtf = richRtfWrite(&rt, RT_SANS, &len);
    +    TEST_TRUE(richRtfRead(&back, rtf, len));
    +    TEST_TRUE(sameText(&rt, &back));
    +    free(rtf);
    +    TEST_FALSE(richRtfRead(&back, "plain", 5));
    +    TEST_FALSE(richRtfIs(NULL, 0));
    +
    +    richTextFree(&rt);
    +    richTextFree(&back);
    +}
    +
    +
    +// A word processor's RTF: its default font is the default, sizes in
    +// pixels, styles and colors, alignment, Windows-1252 and Unicode
    +// characters (one with no code page 437 character: '?'), its other
    +// destinations skipped; and BASIC's conversions
    +static void testRtfForeign(void) {
    +    RichTextT rt;
    +    char      want[] = { 'H', 'e', 'l', 'l', 'o', ' ', 'b', 'o', 'l', 'd', ' ', 'a', 'n', 'd', ' ', 'r', 'e', 'd', '.', '\n', 'C', 'a', 'f', (char)RT_E_ACUTE, ' ', '?', '\0' };
    +
    +    richTextInit(&rt);
    +    TEST_TRUE(richRtfRead(&rt, sWordPad, (int32_t)strlen(sWordPad)));
    +    TEST_EQ(richTextLength(&rt), (int32_t)strlen(want));
    +    TEST_TRUE(memcmp(rt.text, want, strlen(want)) == 0);
    +    TEST_EQ(richTextParaCount(&rt), 2);
    +    TEST_EQ(rt.aligns[0], FLOW_ALIGN_LEFT);
    +    TEST_EQ(rt.aligns[1], FLOW_ALIGN_CENTER);
    +
    +    const RichStyleT *hello = &rt.styles[richTextStyleAt(&rt, 0)];
    +    const RichStyleT *b     = &rt.styles[richTextStyleAt(&rt, 6)];
    +    const RichStyleT *red   = &rt.styles[richTextStyleAt(&rt, 15)];
    +    const RichStyleT *cafe  = &rt.styles[richTextStyleAt(&rt, 20)];
    +
    +    TEST_STR_EQ(hello->family, "");
    +    TEST_EQ(hello->size, RT_WORDPAD_SIZE);
    +    TEST_EQ(hello->style, 0);
    +    TEST_EQ(b->style, FONT_STYLE_BOLD);
    +    TEST_EQ(red->color, RT_RED);
    +    TEST_EQ(hello->color, RICH_COLOR_DEFAULT);
    +    TEST_STR_EQ(cafe->family, "Times New Roman");
    +    TEST_EQ(cafe->size, RT_WORDPAD_BIG);
    +    richTextFree(&rt);
    +
    +    // BASIC: plain text there and back, RTF's characters included
    +    TEST_STR_EQ(RtfToText(TextToRtf("a\nb{c}\\")), "a\nb{c}\\");
    +    TEST_STR_EQ(RtfToText("not RTF"), "not RTF");
    +    TEST_STR_EQ(RtfToText(sWordPad), want);
    +}
    diff --git a/src/test/unit/unitMain.c b/src/test/unit/unitMain.c
    index 4d1af8ee..88a5cb95 100644
    --- a/src/test/unit/unitMain.c
    +++ b/src/test/unit/unitMain.c
    @@ -72,12 +72,15 @@ static const UnitSectionT sSections[] = {
         { "comp",     testComp },
         { "draw",     testDraw },
         { "event",    testEvent },
    +    { "flow",     testFlow },
    +    { "fonts",    testFonts },
         { "iface",    testIface },
         { "layout",   testLayout },
         { "mem",      testMem },
         { "platform", testPlatform },
         { "prefs",    testPrefs },
         { "resource", testResource },
    +    { "richtext", testRichText },
         { "util",     testUtil },
         { "video",    testVideo },
         { "wm",       testWm },
    diff --git a/src/test/unit/unitTest.h b/src/test/unit/unitTest.h
    index d088b0c9..18684d4c 100644
    --- a/src/test/unit/unitTest.h
    +++ b/src/test/unit/unitTest.h
    @@ -72,12 +72,15 @@ void testClock(void);
     void testComp(void);
     void testDraw(void);
     void testEvent(void);
    +void testFlow(void);
    +void testFonts(void);
     void testIface(void);
     void testLayout(void);
     void testMem(void);
     void testPlatform(void);
     void testPrefs(void);
     void testResource(void);
    +void testRichText(void);
     void testUtil(void);
     void testVideo(void);
     void testWm(void);
    diff --git a/src/test/widgets/Makefile b/src/test/widgets/Makefile
    index f06b30bc..921429ca 100644
    --- a/src/test/widgets/Makefile
    +++ b/src/test/widgets/Makefile
    @@ -32,7 +32,7 @@ include ../dvxtest/dvxtest.mk
     
     BINDIR  = $(DVXTEST_BINDIR)/widgets
     TARGET  = $(BINDIR)/widgets
    -SRCS    = wtCommon.c testData.c testProps.c testInput.c testLists.c testRange.c testMisc.c testFocus.c testDestroy.c testHover.c testGeneric.c testCovListView.c testCovTreeView.c testCovTerm.c testCovPopups.c testCovMisc.c testCovText.c testHelpView.c widgetsMain.c
    +SRCS    = wtCommon.c testData.c testProps.c testInput.c testLists.c testRange.c testMisc.c testFocus.c testDestroy.c testHover.c testGeneric.c testCovListView.c testCovTreeView.c testCovTerm.c testCovPopups.c testCovMisc.c testCovText.c testHelpView.c testRichEdit.c widgetsMain.c
     OBJS    = $(patsubst %.c,$(BINDIR)/%.o,$(SRCS))
     
     .PHONY: all run clean
    diff --git a/src/test/widgets/testCovTerm.c b/src/test/widgets/testCovTerm.c
    index e8aa5546..7ddd06ea 100644
    --- a/src/test/widgets/testCovTerm.c
    +++ b/src/test/widgets/testCovTerm.c
    @@ -32,7 +32,9 @@
     #include "ansiTerm/ansiTerm.h"
     #include "scrollPane/scrlPane.h"
     
    +#include 
     #include 
    +#include 
     #include 
     
     #define TERM_COLS          20
    @@ -55,6 +57,12 @@
     #define TERM_WHEEL_NOTCH   1
     #define TERM_HELLO_LEN     5
     #define TERM_SCHEME_BASE   10
    +#define TERM_SCROLL_ONE    1
    +#define TERM_SCROLL_SOME   3
    +#define TERM_LINE_MAX      32
    +#define TERM_ANSI_COLORS   7     // SGR 31..37
    +#define TERM_MACRO_MAX     64    // the longest music string a case expects
    +#define TERM_MUSIC_LONG    4200  // more than the widget's ANSI_MUSIC_MAX (4096)
     
     typedef struct {
         uint8_t feed[TERM_FEED_MAX];
    @@ -64,6 +72,26 @@ typedef struct {
         int32_t sentLen;
     } FakeCommT;
     
    +// What the Music event's handler does besides counting (recordEvent)
    +typedef enum {
    +    TermActNoneE,
    +    TermActWriteE,      // writes "Z" to the terminal
    +    TermActPollE,       // gives the connection a "Q" and polls
    +    TermActDestroyE     // destroys the terminal
    +} TermActE;
    +
    +// The Music and Bell events the terminal fired (recordEvent)
    +typedef struct {
    +    int32_t    bells;
    +    int32_t    musics;
    +    char       macro[TERM_MACRO_MAX];
    +    TermActE   action;
    +    FakeCommT *comm;
    +    int32_t    pollRead;    // what the Poll in the handler read
    +} TermEventsT;
    +
    +static TermEventsT sEvents;
    +
     
     // ============================================================
     // Prototypes
    @@ -71,9 +99,11 @@ typedef struct {
     
     static void    caseTermCursorOps(void);
     static void    caseTermExtensions(void);
    +static void    caseTermMusic(void);
     static void    caseTermRegressions(void);
     static void    caseTermResizeHistory(void);
     static void    caseTermScrollbackKeys(void);
    +static void    caseTermScrollPaint(void);
     static void    caseTermScrollRegion(void);
     static void    caseTermSelection(void);
     static void    caseTermSgrBlink(void);
    @@ -84,7 +114,10 @@ static void    expectCursor(FakeCommT *c, WidgetT *term, int32_t row, int32_t co
     static int32_t fakeRead(void *ctx, uint8_t *buf, int32_t max);
     static int32_t fakeWrite(void *ctx, const uint8_t *data, int32_t len);
     static void    feedTerm(FakeCommT *c, WidgetT *term, const char *text);
    +static void    recordEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc);
    +static bool    rowIs(WindowT *win, WidgetT *term, int32_t row, const char *expect);
     static bool    sentIs(const FakeCommT *c, const char *expect);
    +static bool    termMatchesFullPaint(WindowT *win, WidgetT *term);
     
     
     static void caseTermCursorOps(void) {
    @@ -213,6 +246,91 @@ static void caseTermExtensions(void) {
     }
     
     
    +// ANSI music: ESC[| and ESC[N always, ESC[M with a mode letter after it
    +// (bare ESC[M otherwise still deletes a line), to Ctrl+N, kept off the
    +// screen and handed to the Music event; BEL to the Bell event.  While a
    +// handler runs, a Write waits behind the rest of the input and Poll reads
    +// nothing; a handler may destroy the terminal.
    +static void caseTermMusic(void) {
    +    tCase("ansiTerm: ANSI music and bell events, input held while they run");
    +
    +    WtFixtureT f    = wtFixture("TermMusic");
    +    WidgetT   *term = wgtAnsiTerm(f.root, TERM_COLS, TERM_ROWS);
    +    FakeCommT  comm;
    +    WgtValueT  v;
    +    uint8_t    longTune[TERM_MUSIC_LONG];
    +    int32_t    longLen = 0;
    +
    +    memset(&comm, 0, sizeof(comm));
    +    memset(&sEvents, 0, sizeof(sEvents));
    +    sEvents.comm = &comm;
    +    wgtAnsiTermSetComm(term, &comm, fakeRead, fakeWrite);
    +    term->onEvent = recordEvent;
    +    wtSettle();
    +    TEST_TRUE(wgtGetProp(term, "Music", &v) && v.i);
    +
    +    feedTerm(&comm, term, "\fAB\x1b[MFT120O4\r\nL8CDE\x0eXY");
    +    TEST_EQ(sEvents.musics, 1);
    +    TEST_STR_EQ(sEvents.macro, "MFT120O4L8CDE");
    +    TEST_TRUE(rowIs(f.win, term, 0, "ABXY"));
    +    feedTerm(&comm, term, "\x1b[NT200C\x0e");
    +    TEST_STR_EQ(sEvents.macro, "T200C");
    +    feedTerm(&comm, term, "\x1b[|O2E\x0e");
    +    TEST_STR_EQ(sEvents.macro, "O2E");
    +    feedTerm(&comm, term, "\x1b[Mbl4G\x0e");
    +    TEST_EQ(sEvents.musics, 4);
    +    TEST_STR_EQ(sEvents.macro, "MBl4G");
    +
    +    // Bare ESC[M before anything but a mode letter: Delete Line, then the letter
    +    feedTerm(&comm, term, "\f111\r\n222\r\n333\x1b[1;1H\x1b[MX");
    +    TEST_TRUE(rowIs(f.win, term, 0, "X22"));
    +    TEST_EQ(sEvents.musics, 4);
    +
    +    // ESC abandons a string for the sequence it starts; empty and too
    +    // long strings fire nothing
    +    feedTerm(&comm, term, "\x1b[MFCDE\x1b[1;1HOK");
    +    TEST_TRUE(rowIs(f.win, term, 0, "OK2"));
    +    feedTerm(&comm, term, "\x1b[N\x0e");
    +    memcpy(longTune, "\x1b[N", 3);
    +    longLen = 3;
    +    memset(longTune + longLen, 'C', TERM_MUSIC_LONG - longLen - 1);
    +    longTune[TERM_MUSIC_LONG - 1] = 0x0E;
    +    wgtAnsiTermWrite(term, longTune, TERM_MUSIC_LONG);
    +    feedTerm(&comm, term, "\x1b[1;1HZZ");
    +    TEST_TRUE(rowIs(f.win, term, 0, "ZZ2"));
    +    TEST_EQ(sEvents.musics, 4);
    +
    +    feedTerm(&comm, term, "\a\a");
    +    TEST_EQ(sEvents.bells, 2);
    +
    +    // The handler's Write comes after the rest of the input, and its Poll
    +    // leaves the connection alone until it returns
    +    sEvents.action = TermActWriteE;
    +    feedTerm(&comm, term, "\f\x1b[NC\x0eXY");
    +    TEST_TRUE(rowIs(f.win, term, 0, "XYZ"));
    +    sEvents.action = TermActPollE;
    +    feedTerm(&comm, term, "\f\x1b[NC\x0eXY");
    +    TEST_EQ(sEvents.pollRead, 0);
    +    TEST_TRUE(rowIs(f.win, term, 0, "XYQ"));
    +    TEST_EQ(sEvents.musics, 6);
    +
    +    // Off: no music (ESC[M deletes a line)
    +    sEvents.action = TermActNoneE;
    +    wgtAnsiTermSetMusic(term, false);
    +    TEST_TRUE(wgtGetProp(term, "Music", &v) && !v.i);
    +    feedTerm(&comm, term, "\f\x1b[MFC\x0e\x1b[NG\x0e\x1b[|E\x0e");
    +    TEST_EQ(sEvents.musics, 6);
    +    wgtAnsiTermSetMusic(term, true);
    +    wgtAnsiTermSetMusic(f.root, false);
    +
    +    // A handler that destroys the terminal: the rest of the input goes with it
    +    sEvents.action = TermActDestroyE;
    +    wgtAnsiTermWrite(term, (const uint8_t *)"\x1b[NC\x0eMORE", (int32_t)strlen("\x1b[NC\x0eMORE"));
    +    TEST_EQ(sEvents.musics, 7);
    +    tShutdown();
    +}
    +
    +
     static void caseTermRegressions(void) {
         tCase("ansiTerm: IND/RI, wheel, Shift+PgUp, stale selection, hidden repaint");
     
    @@ -417,6 +535,51 @@ static void caseTermScrollbackKeys(void) {
     }
     
     
    +// Scrolling between repaints: the quick repaint moves the rows it drew
    +// before up and draws the rest -- the same pixels a full paint draws,
    +// after one scroll, several, and more than a screen of them.  The cursor
    +// is hidden so its blink cannot differ between the two.
    +static void caseTermScrollPaint(void) {
    +    tCase("ansiTerm: quick repaint after scrolls matches a full paint");
    +
    +    WtFixtureT    f       = wtFixture("TermScr");
    +    WidgetT      *term    = wgtAnsiTerm(f.root, TERM_COLS, TERM_ROWS);
    +    const int32_t steps[] = { TERM_SCROLL_ONE, TERM_SCROLL_SOME, TERM_ROWS + TERM_SCROLL_SOME };
    +    FakeCommT     comm;
    +
    +    int32_t       lineNo  = 0;
    +
    +    memset(&comm, 0, sizeof(comm));
    +    wgtAnsiTermSetComm(term, &comm, fakeRead, fakeWrite);
    +    feedTerm(&comm, term, "\x1b[?25l");
    +
    +    // A full screen first, so every later line scrolls; each line is
    +    // different, so a row left where it was cannot pass for the right one
    +    for (int32_t s = -1; s < DVX_ARRAY_LEN(steps); s++) {
    +        int32_t count = (s < 0) ? TERM_ROWS : steps[s];
    +        int32_t outY  = 0;
    +        int32_t outH  = 0;
    +
    +        for (int32_t i = 0; i < count; i++, lineNo++) {
    +            char line[TERM_LINE_MAX];
    +
    +            snprintf(line, sizeof(line), "\x1b[3%" PRId32 "mline %" PRId32 "\r\n", lineNo % TERM_ANSI_COLORS + 1, lineNo);
    +            feedTerm(&comm, term, line);
    +        }
    +
    +        if (s < 0) {
    +            wtSettle();
    +            continue;
    +        }
    +
    +        TEST_TRUE(wgtAnsiTermRepaint(term, &outY, &outH) > 0);
    +        TEST_TRUE(termMatchesFullPaint(f.win, term));
    +    }
    +
    +    tShutdown();
    +}
    +
    +
     static void caseTermScrollRegion(void) {
         tCase("ansiTerm: scroll regions, origin mode, index, IL memmove, RIS");
     
    @@ -663,6 +826,50 @@ static void feedTerm(FakeCommT *c, WidgetT *term, const char *text) {
     }
     
     
    +// The Music and Bell events: counted, the string kept, and the action of
    +// the case done
    +static void recordEvent(WidgetT *w, const char *name, WgtValueT *args, int32_t argc) {
    +    if (strcmp(name, ANSITERM_EVT_BELL) == 0) {
    +        sEvents.bells++;
    +        return;
    +    }
    +
    +    if (strcmp(name, ANSITERM_EVT_MUSIC) != 0) {
    +        return;
    +    }
    +
    +    sEvents.musics++;
    +    snprintf(sEvents.macro, sizeof(sEvents.macro), "%s", (argc == 1 && args[0].type == WGT_IFACE_STRING) ? args[0].s : "?");
    +
    +    switch (sEvents.action) {
    +        case TermActWriteE:
    +            wgtAnsiTermWrite(w, (const uint8_t *)"Z", 1);
    +            break;
    +
    +        case TermActPollE:
    +            sEvents.comm->feed[sEvents.comm->feedLen++] = 'Q';
    +            sEvents.pollRead = wgtAnsiTermPoll(w);
    +            break;
    +
    +        case TermActDestroyE:
    +            wgtDestroy(w);
    +            break;
    +
    +        case TermActNoneE:
    +            break;
    +    }
    +}
    +
    +
    +// The first strlen(expect) cells of screen row row, copied, are expect
    +static bool rowIs(WindowT *win, WidgetT *term, int32_t row, const char *expect) {
    +    int32_t len = (int32_t)strlen(expect);
    +
    +    wtDrag(win, cellX(term, 0), cellY(term, row), cellX(term, len), cellY(term, row));
    +    return wgtAnsiTermCopy(term) && clipIs(expect);
    +}
    +
    +
     static bool sentIs(const FakeCommT *c, const char *expect) {
         int32_t len = (int32_t)strlen(expect);
     
    @@ -681,11 +888,37 @@ static bool sentIs(const FakeCommT *c, const char *expect) {
     }
     
     
    +// Whether term's pixels in win's content buffer are what a full paint of
    +// term draws (it is painted to see)
    +static bool termMatchesFullPaint(WindowT *win, WidgetT *term) {
    +    int32_t  bpp     = tCtx()->blitOps.bytesPerPixel;
    +    int32_t  rowSize = term->w * bpp;
    +    uint8_t *before  = (uint8_t *)malloc((size_t)(rowSize * term->h));
    +    bool     same    = before != NULL;
    +
    +    for (int32_t y = 0; same && y < term->h; y++) {
    +        memcpy(before + y * rowSize, win->contentBuf + (term->y + y) * win->contentPitch + term->x * bpp, (size_t)rowSize);
    +    }
    +
    +    wgtInvalidatePaint(term);
    +    wtSettle();
    +
    +    for (int32_t y = 0; same && y < term->h; y++) {
    +        same = memcmp(before + y * rowSize, win->contentBuf + (term->y + y) * win->contentPitch + term->x * bpp, (size_t)rowSize) == 0;
    +    }
    +
    +    free(before);
    +    return same;
    +}
    +
    +
     void wtSectionCovTerm(void) {
         caseTermCursorOps();
         caseTermExtensions();
    +    caseTermMusic();
         caseTermRegressions();
         caseTermResizeHistory();
    +    caseTermScrollPaint();
         caseTermScrollRegion();
         caseTermScrollbackKeys();
         caseTermSelection();
    diff --git a/src/test/widgets/testDestroy.c b/src/test/widgets/testDestroy.c
    index 3dc26f6d..0eace99f 100644
    --- a/src/test/widgets/testDestroy.c
    +++ b/src/test/widgets/testDestroy.c
    @@ -55,14 +55,10 @@ typedef enum {
     
     static const char *sTriggerNames[TriggerCountE] = { "onClick", "onMouseDown", "onFocus", "onKeyDown", "onBlur", "onChange" };
     
    -// NEEDS(widgetTextInput.c): widgetTextAreaOnKey writes ta->cachedGutterW
    -// after widgetTextEditMultiOnKey returns, so a Change handler that destroys
    -// the text area is still a use-after-free in the caller.  Until that write
    -// is gated on sWidgetGen, typing keys are not sent to the text area.
    -
     // Widgets whose onChange a key or the clock can trigger; each must survive
    -// destroying itself from that handler.
    -static const char *const sChangeReachable[] = { "checkbox", "radio", "spinner", "slider", "tabcontrol", "listbox", "listview", "treeview", "dropdown", "combobox", "timer", NULL };
    +// destroying itself from that handler (the text boxes when a typed key
    +// changes their text).
    +static const char *const sChangeReachable[] = { "checkbox", "radio", "spinner", "slider", "tabcontrol", "listbox", "listview", "treeview", "dropdown", "combobox", "textbox", "textarea", "richedit", "timer", NULL };
     
     
     // ============================================================
    diff --git a/src/test/widgets/testMisc.c b/src/test/widgets/testMisc.c
    index ff4ba330..8514e9d3 100644
    --- a/src/test/widgets/testMisc.c
    +++ b/src/test/widgets/testMisc.c
    @@ -33,12 +33,14 @@
     #include "image/image.h"
     #include "imageButton/imgBtn.h"
     #include "label/label.h"
    +#include "richLabel/richLbl.h"
     #include "separator/separatr.h"
     #include "spacer/spacer.h"
     #include "statusBar/statBar.h"
     #include "timer/timer.h"
     #include "toolbar/toolbar.h"
     #include "wrapBox/wrapBox.h"
    +#include "dvxText.h"
     
     #include 
     #include 
    @@ -79,6 +81,11 @@
     #define CV_PEN_MAX         255
     #define CV_TEXT_X          2
     #define CV_TEXT_Y          2
    +#define RICH_RED           0x00FF0000u
    +#define RICH_BAS_RED       0x0000FF  // RGB(255, 0, 0) as BASIC writes it
    +#define RICH_BAS_DEFAULT   (-1)      // SetColor -1: the ForeColor
    +#define RICH_WRAP_COLS     4         // a width the first line's text does not fit in
    +#define RICH_BAD_ALIGN     7
     
     typedef struct {
         uint8_t feed[TERM_FEED_MAX];
    @@ -101,6 +108,7 @@ static void     caseDataWidgets(void);
     static void     caseImage(void);
     static void     caseImageButton(void);
     static void     caseImageRepaint(void);
    +static void     caseRichLabel(void);
     static void     caseSeparatorSpacer(void);
     static void     caseStatusBar(void);
     static void     caseTimer(void);
    @@ -116,6 +124,7 @@ static void     onCanvasDestroy(WidgetT *w, int32_t cx, int32_t cy, bool drag);
     static void     onCanvasMouse(WidgetT *w, int32_t cx, int32_t cy, bool drag);
     static void     onNestedTick(WidgetT *w);
     static void     onTick(WidgetT *w);
    +static int32_t  rectCount(const WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t pixel);
     static bool     sentIs(const FakeCommT *c, const char *expect);
     
     
    @@ -618,6 +627,97 @@ static void caseImageRepaint(void) {
     }
     
     
    +// RichLabel: runs in styles and colors of their own, its natural size,
    +// wrapping to its Width, painting, Clear, and the methods BASIC calls
    +// (with the system font, the only family a bare test has: bold is made)
    +static void caseRichLabel(void) {
    +    tCase("richLabel: runs, size, wrap, paint, clear, BASIC methods");
    +
    +    WtFixtureT   f     = wtFixture("Rich");
    +    WidgetT     *rl    = wgtRichLabel(f.root);
    +    BitmapFontT *bold  = dvxFontOpen(FONT_SYSTEM_FAMILY, FONT_CHAR_HEIGHT, FONT_STYLE_BOLD);
    +    uint32_t     red   = packColor(&tCtx()->display, 0xFF, 0x00, 0x00);
    +    int32_t      lineW = textWidth(&tCtx()->font, "ab ") + textWidth(bold, "cd");
    +
    +    TEST_TRUE(rl != NULL && bold != NULL);
    +
    +    if (!rl || !bold) {
    +        dvxFontClose(bold);
    +        tShutdown();
    +        return;
    +    }
    +
    +    // "ab cd" (cd bold), then a centered paragraph "ef" in red
    +    wgtRichLabelAddText(rl, "ab ");
    +    wgtRichLabelSetStyle(rl, FONT_STYLE_BOLD);
    +    wgtRichLabelAddText(rl, "cd");
    +    wgtRichLabelSetStyle(rl, 0);
    +    wgtRichLabelSetColor(rl, RICH_RED);
    +    wgtRichLabelAddParagraph(rl, FLOW_ALIGN_CENTER);
    +    wgtRichLabelAddParagraph(rl, RICH_BAD_ALIGN);
    +    wgtRichLabelAddText(rl, "ef");
    +    wtSettle();
    +    TEST_EQ(rl->calcMinW, lineW);
    +    TEST_EQ(rl->calcMinH, 2 * FONT_CHAR_HEIGHT);
    +    TEST_EQ(rectCount(f.win, rl->x, rl->y, rl->w, FONT_CHAR_HEIGHT, red), 0);
    +    TEST_TRUE(rectCount(f.win, rl->x, rl->y + FONT_CHAR_HEIGHT, rl->w, FONT_CHAR_HEIGHT, red) > 0);
    +
    +    // The red run sits in the middle of the line
    +    TEST_EQ(rectCount(f.win, rl->x, rl->y + FONT_CHAR_HEIGHT, (rl->w - textWidth(&tCtx()->font, "ef")) / 2, FONT_CHAR_HEIGHT, red), 0);
    +
    +    // Too narrow for "ab cd": cd wraps
    +    rl->prefW = wgtPixels(RICH_WRAP_COLS * FONT_CHAR_WIDTH);
    +    wgtInvalidate(rl);
    +    wtSettle();
    +    TEST_EQ(rl->calcMinW, RICH_WRAP_COLS * FONT_CHAR_WIDTH);
    +    TEST_EQ(rl->calcMinH, 3 * FONT_CHAR_HEIGHT);
    +
    +    // The same with only minW (what BASIC's Width sets): cd wraps there
    +    // too, painted as measured although the layout stretches the widget
    +    // wider (the red ef on the third line)
    +    rl->prefW = 0;
    +    rl->minW  = wgtPixels(RICH_WRAP_COLS * FONT_CHAR_WIDTH);
    +    wgtInvalidate(rl);
    +    wtSettle();
    +    TEST_EQ(rl->calcMinW, RICH_WRAP_COLS * FONT_CHAR_WIDTH);
    +    TEST_EQ(rl->calcMinH, 3 * FONT_CHAR_HEIGHT);
    +    TEST_TRUE(rl->w > RICH_WRAP_COLS * FONT_CHAR_WIDTH);
    +    TEST_EQ(rectCount(f.win, rl->x, rl->y + FONT_CHAR_HEIGHT, rl->w, FONT_CHAR_HEIGHT, red), 0);
    +    TEST_TRUE(rectCount(f.win, rl->x, rl->y + 2 * FONT_CHAR_HEIGHT, rl->w, FONT_CHAR_HEIGHT, red) > 0);
    +
    +    // Empty: one line of its font tall, nothing wide, the style and
    +    // color back to the control's
    +    rl->minW = 0;
    +    wgtRichLabelClear(rl);
    +    wtSettle();
    +    TEST_EQ(rl->calcMinW, 0);
    +    TEST_EQ(rl->calcMinH, FONT_CHAR_HEIGHT);
    +
    +    // What BASIC calls: SetColor takes a BASIC color, -1 the ForeColor
    +    const WgtIfaceT      *iface   = wgtGetIface("richlabel");
    +    const WgtMethodDescT *addText = findMethod(iface, "AddText");
    +    const WgtMethodDescT *setCol  = findMethod(iface, "SetColor");
    +    const WgtMethodDescT *setFont = findMethod(iface, "SetFont");
    +
    +    TEST_TRUE(addText && setCol && setFont);
    +
    +    if (addText && setCol && setFont) {
    +        ((void (*)(WidgetT *, int32_t))setCol->fn)(rl, RICH_BAS_RED);
    +        ((void (*)(WidgetT *, const char *))addText->fn)(rl, "gh");
    +        ((void (*)(WidgetT *, int32_t))setCol->fn)(rl, RICH_BAS_DEFAULT);
    +        ((void (*)(WidgetT *, const char *, int32_t))setFont->fn)(rl, "", 0);
    +        ((void (*)(WidgetT *, const char *))addText->fn)(rl, "ij");
    +        wtSettle();
    +        TEST_EQ(rl->calcMinW, textWidth(&tCtx()->font, "ghij"));
    +        TEST_TRUE(rectCount(f.win, rl->x, rl->y, textWidth(&tCtx()->font, "gh"), FONT_CHAR_HEIGHT, red) > 0);
    +        TEST_EQ(rectCount(f.win, rl->x + textWidth(&tCtx()->font, "gh"), rl->y, textWidth(&tCtx()->font, "ij"), FONT_CHAR_HEIGHT, red), 0);
    +    }
    +
    +    dvxFontClose(bold);
    +    tShutdown();
    +}
    +
    +
     static void caseSeparatorSpacer(void) {
         tCase("separator/spacer: sizes and fill weight");
     
    @@ -999,6 +1099,20 @@ static void onTick(WidgetT *w) {
     }
     
     
    +// How many content pixels of the rectangle are pixel
    +static int32_t rectCount(const WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t pixel) {
    +    int32_t count = 0;
    +
    +    for (int32_t row = y; row < y + h; row++) {
    +        for (int32_t col = x; col < x + w; col++) {
    +            count += (wtContentPixel(win, col, row) == pixel) ? 1 : 0;
    +        }
    +    }
    +
    +    return count;
    +}
    +
    +
     static bool sentIs(const FakeCommT *c, const char *expect) {
         int32_t len = (int32_t)strlen(expect);
     
    @@ -1028,6 +1142,7 @@ void wtSectionMisc(void) {
         caseCanvasClip();
         caseCanvasDestroyInDrag();
         caseWrapBox();
    +    caseRichLabel();
         caseTimer();
         caseTimerNestedLoop();
         caseAnsiTerm();
    diff --git a/src/test/widgets/testProps.c b/src/test/widgets/testProps.c
    index 81a8db24..de335d01 100644
    --- a/src/test/widgets/testProps.c
    +++ b/src/test/widgets/testProps.c
    @@ -85,8 +85,10 @@ static const char *sCommonEvents[] = { "Click", "DblClick", "Change", "GotFocus"
     
     // String setters that do not store the string as given: Picture loads a
     // file (the getter reports what was loaded), PasswordChar keeps one
    -// character.  Only the fixed-point rule applies to them.
    -static const char *sFixedPointProps[] = { "PasswordChar", "Picture", "SelText", NULL };
    +// character, SelFontName reads "" (the control's own family) as that
    +// family's name, and TextRTF and SelRTF take text and give RTF.  Only
    +// the fixed-point rule applies to them.
    +static const char *sFixedPointProps[] = { "PasswordChar", "Picture", "SelFontName", "SelRTF", "SelText", "TextRTF", NULL };
     
     
     // ============================================================
    diff --git a/src/test/widgets/testRichEdit.c b/src/test/widgets/testRichEdit.c
    new file mode 100644
    index 00000000..b72c80a4
    --- /dev/null
    +++ b/src/test/widgets/testRichEdit.c
    @@ -0,0 +1,520 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// testRichEdit.c  -- widgets section: the RichEdit control
    +//
    +// In the system font (8-pixel cells, 16-pixel lines) so every place is a
    +// count of characters: text and selection, typing and editing keys with
    +// undo, styles and alignment, RTF, the clipboard with styles, the mouse,
    +// find, files, and the repaint of only the rows a change touched.
    +
    +#include "wtCommon.h"
    +#include "richEdit/richEdit.h"
    +#include "dvxFlow.h"
    +#include "dvxText.h"
    +#include "dvxVideo.h"
    +#include "textHelp.h"
    +
    +#include 
    +#include 
    +
    +#define RE_CW            FONT_CHAR_WIDTH
    +#define RE_LINE          FONT_CHAR_HEIGHT
    +#define RE_INSET         4          // border and padding before the text
    +#define RE_RED           0x00FF0000u
    +#define RE_SB_W          14         // WGT_SB_W
    +#define RE_PARAS         40         // enough paragraphs to scroll
    +#define RE_KEY_BOLD      0x02       // Ctrl+B
    +#define RE_KEY_RIGHT     0x12       // Ctrl+R
    +#define RE_PATH_MAX      256
    +#define RE_CLICK_GAP_MS  60         // within the double-click time: clicks chain
    +
    +static int32_t sChanges = 0;
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +static void     caseAlign(void);
    +static void     caseClipboard(void);
    +static void     caseFindFiles(void);
    +static void     caseKeys(void);
    +static void     caseMouse(void);
    +static void     casePaint(void);
    +static void     caseRtf(void);
    +static void     caseText(void);
    +static void     countChange(WidgetT *w);
    +static WidgetT *makeEdit(WtFixtureT *f, const char *title);
    +static int32_t  rowCount(const WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t pixel);
    +static void     typeKeys(WindowT *win, const char *text);
    +void            wtSectionRichEdit(void);
    +
    +
    +// Alignment per paragraph: the caret's, or the selection's; undone
    +static void caseAlign(void) {
    +    tCase("richEdit: paragraph alignment, undo, Ctrl+R");
    +
    +    WtFixtureT f;
    +    WidgetT   *re = makeEdit(&f, "RichAlign");
    +
    +    wgtSetText(re, "ab\ncd");
    +    wgtRichEditSetSelStart(re, 4);
    +    wgtRichEditSetSelAlign(re, FLOW_ALIGN_CENTER);
    +    TEST_EQ(wgtRichEditGetSelAlign(re), FLOW_ALIGN_CENTER);
    +    wgtRichEditSetSelStart(re, 0);
    +    TEST_EQ(wgtRichEditGetSelAlign(re), FLOW_ALIGN_LEFT);
    +    wgtRichEditUndo(re);
    +    wgtRichEditSetSelStart(re, 4);
    +    TEST_EQ(wgtRichEditGetSelAlign(re), FLOW_ALIGN_LEFT);
    +    wgtRichEditRedo(re);
    +    wgtRichEditSetSelStart(re, 4);
    +    TEST_EQ(wgtRichEditGetSelAlign(re), FLOW_ALIGN_CENTER);
    +
    +    // A selection over both paragraphs; the keys
    +    wgtRichEditSetSelStart(re, 1);
    +    wgtRichEditSetSelLength(re, 3);
    +    tWidgetKey(f.win, RE_KEY_RIGHT, KEY_MOD_CTRL);
    +    wgtRichEditSetSelStart(re, 0);
    +    TEST_EQ(wgtRichEditGetSelAlign(re), FLOW_ALIGN_RIGHT);
    +    wgtRichEditSetSelStart(re, 5);
    +    TEST_EQ(wgtRichEditGetSelAlign(re), FLOW_ALIGN_RIGHT);
    +    tShutdown();
    +}
    +
    +
    +// Copy keeps the styles for a paste of the same text; other clipboard
    +// text pastes in the style typing uses; Cut
    +static void caseClipboard(void) {
    +    tCase("richEdit: copy and paste with styles, plain paste, cut");
    +
    +    WtFixtureT f;
    +    WidgetT   *re = makeEdit(&f, "RichClip");
    +
    +    wgtSetText(re, "abc");
    +    wgtRichEditSetSelStart(re, 0);
    +    wgtRichEditSetSelLength(re, 1);
    +    wgtRichEditSetSelStyle(re, FONT_STYLE_BOLD, FONT_STYLE_BOLD);
    +    wgtRichEditSetSelLength(re, 2);
    +    wgtRichEditCopy(re);
    +    wgtRichEditSetSelStart(re, 3);
    +    wgtRichEditPaste(re);
    +    TEST_STR_EQ(wgtGetText(re), "abcab");
    +    wgtRichEditSetSelStart(re, 3);
    +    wgtRichEditSetSelLength(re, 1);
    +    TEST_TRUE(wgtRichEditGetSelStyle(re) & FONT_STYLE_BOLD);
    +    wgtRichEditSetSelStart(re, 4);
    +    wgtRichEditSetSelLength(re, 1);
    +    TEST_TRUE(!(wgtRichEditGetSelStyle(re) & FONT_STYLE_BOLD));
    +
    +    // Text another program put there: in the style typing uses
    +    dvxClipboardCopy("zz", 2);
    +    wgtRichEditSetSelStart(re, 5);
    +    wgtRichEditPaste(re);
    +    TEST_STR_EQ(wgtGetText(re), "abcabzz");
    +
    +    // Cut all, paste it back
    +    wgtRichEditSelectAll(re);
    +    wgtRichEditCut(re);
    +    TEST_STR_EQ(wgtGetText(re), "");
    +    wgtRichEditPaste(re);
    +    TEST_STR_EQ(wgtGetText(re), "abcabzz");
    +    tShutdown();
    +}
    +
    +
    +// Find: from the selection on, any case or matching it, around the end;
    +// files: RTF by extension and back with its styles, plain text, missing
    +static void caseFindFiles(void) {
    +    tCase("richEdit: Find, SaveFile and LoadFile");
    +
    +    WtFixtureT f;
    +    WidgetT   *re = makeEdit(&f, "RichFind");
    +    char       rtfPath[RE_PATH_MAX];
    +    char       txtPath[RE_PATH_MAX];
    +
    +    wgtSetText(re, "one two one");
    +    TEST_EQ(wgtRichEditFind(re, "ONE", false), 0);
    +    TEST_EQ(wgtRichEditGetSelLength(re), 3);
    +    TEST_EQ(wgtRichEditFind(re, "ONE", false), 8);
    +    TEST_EQ(wgtRichEditFind(re, "one", true), 0);
    +    TEST_EQ(wgtRichEditFind(re, "ONE", true), -1);
    +    TEST_EQ(wgtRichEditFind(re, "", false), -1);
    +
    +    snprintf(rtfPath, sizeof(rtfPath), "%s/doc.rtf", tTempDir());
    +    snprintf(txtPath, sizeof(txtPath), "%s/doc.txt", tTempDir());
    +    wgtRichEditSetSelStart(re, 4);
    +    wgtRichEditSetSelLength(re, 3);
    +    wgtRichEditSetSelStyle(re, FONT_STYLE_ITALIC, FONT_STYLE_ITALIC);
    +    TEST_EQ(wgtRichEditSaveFile(re, rtfPath), 0);
    +    TEST_EQ(wgtRichEditSaveFile(re, txtPath), 0);
    +    wgtSetText(re, "");
    +    TEST_EQ(wgtRichEditLoadFile(re, rtfPath), 0);
    +    TEST_STR_EQ(wgtGetText(re), "one two one");
    +    wgtRichEditSetSelStart(re, 4);
    +    wgtRichEditSetSelLength(re, 1);
    +    TEST_TRUE(wgtRichEditGetSelStyle(re) & FONT_STYLE_ITALIC);
    +    TEST_EQ(wgtRichEditLoadFile(re, txtPath), 0);
    +    TEST_STR_EQ(wgtGetText(re), "one two one");
    +    wgtRichEditSetSelStart(re, 4);
    +    wgtRichEditSetSelLength(re, 1);
    +    TEST_TRUE(!(wgtRichEditGetSelStyle(re) & FONT_STYLE_ITALIC));
    +    TEST_EQ(wgtRichEditLoadFile(re, "no/such/file.rtf"), -1);
    +    TEST_EQ(wgtRichEditSaveFile(re, "no/such/dir/file.rtf"), -1);
    +    tShutdown();
    +}
    +
    +
    +// Typing (one undo step), Enter and Backspace across paragraphs, the
    +// moving keys with and without Shift, Ctrl+B, Up / Down keeping the
    +// column, read-only
    +static void caseKeys(void) {
    +    tCase("richEdit: typing, editing and moving keys, undo, read-only");
    +
    +    WtFixtureT f;
    +    WidgetT   *re = makeEdit(&f, "RichKeys");
    +
    +    re->onChange = countChange;
    +    sChanges     = 0;
    +    typeKeys(f.win, "ab");
    +    TEST_STR_EQ(wgtGetText(re), "ab");
    +    TEST_EQ(sChanges, 2);
    +    wgtRichEditUndo(re);
    +    TEST_STR_EQ(wgtGetText(re), "");
    +    wgtRichEditRedo(re);
    +    TEST_STR_EQ(wgtGetText(re), "ab");
    +
    +    tWidgetKey(f.win, KEY_ENTER, 0);
    +    typeKeys(f.win, "c");
    +    TEST_STR_EQ(wgtGetText(re), "ab\nc");
    +    tWidgetKey(f.win, KEY_HOME, 0);
    +    TEST_EQ(wgtRichEditGetSelStart(re), 3);
    +    tWidgetKey(f.win, KEY_BACKSPACE, 0);
    +    TEST_STR_EQ(wgtGetText(re), "abc");
    +    TEST_EQ(wgtRichEditGetSelStart(re), 2);
    +
    +    // Shift+Left twice selects "ab"; Ctrl+B makes it bold; End and a
    +    // typed character take the plain style of "c"
    +    tWidgetKey(f.win, KEY_LEFT, KEY_MOD_SHIFT);
    +    tWidgetKey(f.win, KEY_LEFT, KEY_MOD_SHIFT);
    +    TEST_STR_EQ(wgtRichEditGetSelText(re), "ab");
    +    tWidgetKey(f.win, RE_KEY_BOLD, KEY_MOD_CTRL);
    +    TEST_TRUE(wgtRichEditGetSelStyle(re) & FONT_STYLE_BOLD);
    +    tWidgetKey(f.win, KEY_END, 0);
    +    typeKeys(f.win, "d");
    +    wgtRichEditSetSelStart(re, 3);
    +    wgtRichEditSetSelLength(re, 1);
    +    TEST_TRUE(!(wgtRichEditGetSelStyle(re) & FONT_STYLE_BOLD));
    +
    +    // A style chosen with no selection is the next typed character's
    +    wgtRichEditSetSelStart(re, 4);
    +    wgtRichEditSetSelStyle(re, FONT_STYLE_ITALIC, FONT_STYLE_ITALIC);
    +    typeKeys(f.win, "e");
    +    wgtRichEditSetSelStart(re, 4);
    +    wgtRichEditSetSelLength(re, 1);
    +    TEST_TRUE(wgtRichEditGetSelStyle(re) & FONT_STYLE_ITALIC);
    +
    +    // Ctrl+Home / End, words, Delete
    +    wgtSetText(re, "one two\nthree four");
    +    tWidgetKey(f.win, KEY_CTRL_END, 0);
    +    TEST_EQ(wgtRichEditGetSelStart(re), 18);
    +    tWidgetKey(f.win, KEY_CTRL_LEFT, 0);
    +    TEST_EQ(wgtRichEditGetSelStart(re), 14);
    +    tWidgetKey(f.win, KEY_CTRL_HOME, 0);
    +    tWidgetKey(f.win, KEY_CTRL_RIGHT, 0);
    +    TEST_EQ(wgtRichEditGetSelStart(re), 4);
    +    tWidgetKey(f.win, KEY_DELETE, 0);
    +    TEST_STR_EQ(wgtGetText(re), "one wo\nthree four");
    +
    +    // Down then Up keep the column
    +    wgtRichEditSetSelStart(re, 2);
    +    tWidgetKey(f.win, KEY_DOWN, 0);
    +    TEST_EQ(wgtRichEditGetSelStart(re), 9);
    +    tWidgetKey(f.win, KEY_UP, 0);
    +    TEST_EQ(wgtRichEditGetSelStart(re), 2);
    +    tWidgetKey(f.win, KEY_UP, 0);
    +    TEST_EQ(wgtRichEditGetSelStart(re), 0);
    +
    +    // Read-only: no edits; copying and moving still work
    +    wgtSetReadOnly(re, true);
    +    typeKeys(f.win, "x");
    +    tWidgetKey(f.win, KEY_DELETE, 0);
    +    TEST_STR_EQ(wgtGetText(re), "one wo\nthree four");
    +    tWidgetKey(f.win, KEY_RIGHT, 0);
    +    TEST_EQ(wgtRichEditGetSelStart(re), 1);
    +    tShutdown();
    +}
    +
    +
    +// A click places the caret, Shift+click extends, a drag selects, a
    +// double click selects a word; the scroll bar and the wheel scroll
    +static void caseMouse(void) {
    +    tCase("richEdit: click, drag, double click, scroll bar, wheel");
    +
    +    WtFixtureT f;
    +    WidgetT   *re = makeEdit(&f, "RichMouse");
    +    int32_t    x0;
    +    int32_t    y0;
    +
    +    wgtSetText(re, "Hello world");
    +    wtSettle();
    +    x0 = re->x + RE_INSET;
    +    y0 = re->y + RE_INSET + RE_LINE / 2;
    +    wtClickAt(f.win, x0 + 3 * RE_CW + 1, y0);
    +    TEST_EQ(wgtRichEditGetSelStart(re), 3);
    +    TEST_EQ(wgtRichEditGetSelLength(re), 0);
    +    wtSettle();
    +    wtDrag(f.win, x0, y0, x0 + 5 * RE_CW, y0);
    +    TEST_EQ(wgtRichEditGetSelStart(re), 0);
    +    TEST_EQ(wgtRichEditGetSelLength(re), 5);
    +    wtSettle();
    +    wtClickAt(f.win, x0 + 7 * RE_CW, y0);
    +    tAdvanceClock(RE_CLICK_GAP_MS);
    +    wtClickAt(f.win, x0 + 7 * RE_CW, y0);
    +    TEST_STR_EQ(wgtRichEditGetSelText(re), "world");
    +
    +    // A third click: the paragraph
    +    tAdvanceClock(RE_CLICK_GAP_MS);
    +    wtClickAt(f.win, x0 + 7 * RE_CW, y0);
    +    TEST_STR_EQ(wgtRichEditGetSelText(re), "Hello world");
    +    wtSettle();
    +
    +    // Many paragraphs: the trough below the thumb pages down, so the top
    +    // line is no longer the first
    +    char text[RE_PARAS * 3 + 1];
    +
    +    for (int32_t i = 0; i < RE_PARAS; i++) {
    +        text[i * 3]     = (char)('a' + i % 26);
    +        text[i * 3 + 1] = 'x';
    +        text[i * 3 + 2] = '\n';
    +    }
    +
    +    text[RE_PARAS * 3 - 1] = '\0';
    +    wgtSetText(re, text);
    +    wtSettle();
    +    wtClickAt(f.win, re->x + re->w - RE_SB_W / 2 - 2, re->y + re->h - RE_SB_W * 2);
    +    wtSettle();
    +    wtClickAt(f.win, x0, y0);
    +    TEST_TRUE(wgtRichEditGetSelStart(re) > 0);
    +
    +    // The wheel up a long way: back to the first line
    +    TEST_TRUE(wclsOnWheel(re, -RE_PARAS));
    +    wtSettle();
    +    wtClickAt(f.win, x0, y0);
    +    TEST_EQ(wgtRichEditGetSelStart(re), 0);
    +    tShutdown();
    +}
    +
    +
    +// The quick repaint: an edit inside a line, a move of the caret or the
    +// selection, and the blink paint those rows only; a change of height
    +// repaints the control
    +static void casePaint(void) {
    +    tCase("richEdit: rows repainted, colors drawn, caret blink");
    +
    +    WtFixtureT f;
    +    WidgetT   *re  = makeEdit(&f, "RichPaint");
    +    uint32_t   red = packColor(&tCtx()->display, 0xFF, 0x00, 0x00);
    +    int32_t    y   = 0;
    +    int32_t    h   = 0;
    +
    +    wgtSetText(re, "ab\ncd");
    +    wtSettle();
    +    wgtRichEditSelectAll(re);
    +    wgtRichEditSetSelColor(re, RE_RED);
    +    TEST_EQ(wgtRichEditGetSelColor(re), RE_RED);
    +    wgtRichEditSetSelStart(re, 1);
    +    TEST_EQ(wclsQuickRepaint(re, &y, &h), 1);
    +    TEST_EQ(y, re->y + RE_INSET);
    +    TEST_EQ(h, 2 * RE_LINE);
    +    TEST_TRUE(rowCount(f.win, re->x + RE_INSET, re->y + RE_INSET, 2 * RE_CW, RE_LINE, red) > 0);
    +
    +    // A typed character: its line alone
    +    wgtSetFocused(re);
    +    tWidgetKey(f.win, 'x', 0);
    +    TEST_EQ(wclsQuickRepaint(re, &y, &h), 1);
    +    TEST_EQ(y, re->y + RE_INSET);
    +    TEST_EQ(h, RE_LINE);
    +    TEST_EQ(wclsQuickRepaint(re, &y, &h), 0);
    +
    +    // A new paragraph moves what is below: the whole control repaints
    +    tWidgetKey(f.win, KEY_ENTER, 0);
    +    TEST_EQ(wclsQuickRepaint(re, &y, &h), 0);
    +    TEST_TRUE(re->paintDirty);
    +    wtSettle();
    +
    +    // The blink (each frame's poll and quick repaint): the caret, at the
    +    // start of the second line, shows and hides
    +    int32_t  cx     = re->x + RE_INSET;
    +    int32_t  cy     = re->y + RE_INSET + RE_LINE + RE_LINE / 2;
    +    uint32_t before = wtContentPixel(f.win, cx, cy);
    +
    +    tAdvanceClock(TEXT_CURSOR_BLINK_MS + 1);
    +    TEST_TRUE(wtContentPixel(f.win, cx, cy) != before);
    +    tAdvanceClock(TEXT_CURSOR_BLINK_MS + 1);
    +    TEST_EQ(wtContentPixel(f.win, cx, cy), before);
    +    tShutdown();
    +}
    +
    +
    +// RTF in and out: whole, round trip, the selection's, through the BASIC
    +// property
    +static void caseRtf(void) {
    +    tCase("richEdit: TextRTF and SelRTF");
    +
    +    WtFixtureT f;
    +    WidgetT   *re = makeEdit(&f, "RichRtf");
    +
    +    wgtRichEditSetRtf(re, "{\\rtf1\\ansi{\\fonttbl{\\f0 System;}}{\\colortbl;\\red255\\green0\\blue0;}\\pard\\qc\\cf1 Red\\cf0  plain\\par}");
    +    TEST_STR_EQ(wgtGetText(re), "Red plain");
    +    TEST_EQ(wgtRichEditGetSelColor(re), RE_RED);
    +    TEST_EQ(wgtRichEditGetSelAlign(re), FLOW_ALIGN_CENTER);
    +    TEST_TRUE(strstr(wgtRichEditGetRtf(re), "\\qc") != NULL);
    +    TEST_TRUE(strstr(wgtRichEditGetRtf(re), "\\red255") != NULL);
    +
    +    // Round trip
    +    char saved[RE_PATH_MAX * 4];
    +
    +    snprintf(saved, sizeof(saved), "%s", wgtRichEditGetRtf(re));
    +    wgtSetText(re, "");
    +    wgtRichEditSetRtf(re, saved);
    +    TEST_STR_EQ(wgtGetText(re), "Red plain");
    +    TEST_EQ(wgtRichEditGetSelColor(re), RE_RED);
    +
    +    // The selection's RTF; RTF over the selection
    +    wgtRichEditSetSelStart(re, 4);
    +    wgtRichEditSetSelLength(re, 5);
    +    TEST_TRUE(strstr(wgtRichEditGetSelRtf(re), "plain") != NULL);
    +    wgtRichEditSetSelRtf(re, "{\\rtf1{\\b X}}");
    +    TEST_STR_EQ(wgtGetText(re), "Red X");
    +    wgtRichEditSetSelStart(re, 4);
    +    wgtRichEditSetSelLength(re, 1);
    +    TEST_TRUE(wgtRichEditGetSelStyle(re) & FONT_STYLE_BOLD);
    +
    +    // Not RTF: plain text
    +    wgtRichEditSetRtf(re, "just text");
    +    TEST_STR_EQ(wgtGetText(re), "just text");
    +
    +    // TextRTF through the control's BASIC interface
    +    const WgtIfaceT *iface = wgtGetIface("richedit");
    +    bool             found = false;
    +
    +    for (int32_t i = 0; iface && i < iface->propCount; i++) {
    +        if (strcmp(iface->props[i].name, "TextRTF") == 0) {
    +            const char *(*get)(const WidgetT *) = (const char *(*)(const WidgetT *))iface->props[i].getFn;
    +
    +            found = strstr(get(re), "just text") != NULL;
    +        }
    +    }
    +
    +    TEST_TRUE(found);
    +    tShutdown();
    +}
    +
    +
    +// Text in and out; the selection by offsets; a style on the selection,
    +// undone and redone; SelText replacing it
    +static void caseText(void) {
    +    tCase("richEdit: text, selection, styles, undo and redo");
    +
    +    WtFixtureT f;
    +    WidgetT   *re = makeEdit(&f, "RichText");
    +
    +    wgtSetText(re, "Hello world\nSecond line");
    +    TEST_STR_EQ(wgtGetText(re), "Hello world\nSecond line");
    +    TEST_EQ(wgtRichEditGetSelStart(re), 0);
    +    TEST_TRUE(!(wgtRichEditCanUndo(re)));
    +    wgtRichEditSetSelStart(re, 6);
    +    wgtRichEditSetSelLength(re, 5);
    +    TEST_STR_EQ(wgtRichEditGetSelText(re), "world");
    +    wgtRichEditSetSelStyle(re, FONT_STYLE_BOLD, FONT_STYLE_BOLD);
    +    TEST_TRUE(wgtRichEditGetSelStyle(re) & FONT_STYLE_BOLD);
    +    TEST_EQ(wgtRichEditGetSelStart(re), 6);
    +    TEST_EQ(wgtRichEditGetSelLength(re), 5);
    +    TEST_TRUE(wgtRichEditCanUndo(re));
    +    wgtRichEditUndo(re);
    +    TEST_TRUE(!(wgtRichEditGetSelStyle(re) & FONT_STYLE_BOLD));
    +    TEST_TRUE(wgtRichEditCanRedo(re));
    +    wgtRichEditRedo(re);
    +    TEST_TRUE(wgtRichEditGetSelStyle(re) & FONT_STYLE_BOLD);
    +
    +    // The family and size of the control's own font
    +    TEST_STR_EQ(wgtRichEditGetSelFontName(re), FONT_SYSTEM_FAMILY);
    +    TEST_EQ(wgtRichEditGetSelFontSize(re), FONT_CHAR_HEIGHT);
    +
    +    wgtRichEditSetSelText(re, "there");
    +    TEST_STR_EQ(wgtGetText(re), "Hello there\nSecond line");
    +    TEST_EQ(wgtRichEditGetSelStart(re), 11);
    +    tShutdown();
    +}
    +
    +
    +static void countChange(WidgetT *w) {
    +    (void)w;
    +    sChanges++;
    +}
    +
    +
    +// A RichEdit filling a fresh window, focused
    +static WidgetT *makeEdit(WtFixtureT *f, const char *title) {
    +    *f = wtFixture(title);
    +
    +    WidgetT *re = wgtRichEdit(f->root);
    +
    +    re->weight = WGT_WEIGHT_FILL;
    +    wtSettle();
    +    wgtSetFocused(re);
    +    return re;
    +}
    +
    +
    +// How many content pixels of the rectangle are pixel
    +static int32_t rowCount(const WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t pixel) {
    +    int32_t count = 0;
    +
    +    for (int32_t row = y; row < y + h; row++) {
    +        for (int32_t col = x; col < x + w; col++) {
    +            count += (wtContentPixel(win, col, row) == pixel) ? 1 : 0;
    +        }
    +    }
    +
    +    return count;
    +}
    +
    +
    +// Each character of text as a key to the focused control
    +static void typeKeys(WindowT *win, const char *text) {
    +    for (const char *p = text; *p; p++) {
    +        tWidgetKey(win, (uint8_t)*p, 0);
    +    }
    +}
    +
    +
    +void wtSectionRichEdit(void) {
    +    caseText();
    +    caseKeys();
    +    caseAlign();
    +    caseClipboard();
    +    caseRtf();
    +    caseMouse();
    +    casePaint();
    +    caseFindFiles();
    +}
    diff --git a/src/test/widgets/widgetsMain.c b/src/test/widgets/widgetsMain.c
    index 8f0fd711..c95c1e5c 100644
    --- a/src/test/widgets/widgetsMain.c
    +++ b/src/test/widgets/widgetsMain.c
    @@ -47,5 +47,6 @@ int main(void) {
         wtSectionCovText();
         wtSectionData();
         wtSectionHelpView();
    +    wtSectionRichEdit();
         return tSuiteEnd();
     }
    diff --git a/src/test/widgets/wtCommon.h b/src/test/widgets/wtCommon.h
    index 823641bc..56e5b788 100644
    --- a/src/test/widgets/wtCommon.h
    +++ b/src/test/widgets/wtCommon.h
    @@ -114,5 +114,6 @@ void        wtSectionLists(void);
     void        wtSectionMisc(void);
     void        wtSectionProps(void);
     void        wtSectionRange(void);
    +void        wtSectionRichEdit(void);
     
     #endif // WT_COMMON_H
    diff --git a/src/tools/Makefile b/src/tools/Makefile
    index 2ce829e2..d58ade1b 100644
    --- a/src/tools/Makefile
    +++ b/src/tools/Makefile
    @@ -23,17 +23,20 @@
     # DVX Tools Makefile
     #
     # Builds native (host) utilities into bin/host and cross-compiled DOS
    -# tools into bin/system, and deploys the help sources into bin/.
    +# tools into bin/system, makes the fonts in bin/fonts (mkfont, from
    +# src/fonts/fonts.ini and FONT_SOURCES, common.mk),
    +# and deploys the help sources into bin/.
     
     include ../common.mk
     
     LIBDVX    = ../libs/kpunch/libdvx
     INCLUDES  = -I$(LIBDVX) -I$(LIBDVX)/platform
    +HOST_INCS = $(INCLUDES) -I$(FREETYPE_DIR)/include/freetype2
     
     # Host tools use HOSTCC, never CC, so an analyze.sh / CI override of CC
     # (to a cross/analyzer compiler) cannot cross-compile them or contaminate
     # bin/host.
    -HOSTCFLAGS  = $(HOST_TOOL_CFLAGS) $(INCLUDES)
    +HOSTCFLAGS  = $(HOST_TOOL_CFLAGS) $(HOST_INCS)
     HOSTLDFLAGS =
     DOSCFLAGS   = $(DOS_CFLAGS) $(INCLUDES)
     
    @@ -48,26 +51,27 @@ DOBJDIR   = ../../obj/tools/dos
     ifeq ($(COV),1)
     HOSTDIR     = ../../obj/hostcov/tools/bin
     HOBJDIR     = ../../obj/hostcov/tools/obj
    -HOSTCFLAGS  = $(filter-out -O2,$(HOST_TOOL_CFLAGS)) -O1 -g $(HOST_INSTFLAGS) $(INCLUDES)
    +HOSTCFLAGS  = $(filter-out -O2,$(HOST_TOOL_CFLAGS)) -O1 -g $(HOST_INSTFLAGS) $(HOST_INCS)
     HOSTLDFLAGS = $(HOST_INSTFLAGS)
     endif
     
     # Sources of each tool (host and DOS builds share the lists)
     SUPPORT_SRCS   = $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/thirdparty/stb_ds_impl.c
    -DVXRES_SRCS    = dvxres.c dvxResWrite.c $(LIBDVX)/dvxResource.c $(SUPPORT_SRCS)
    +DVXRES_SRCS    = dvxres.c dvxResWrite.c $(LIBDVX)/dvxResource.c $(LIBDVX)/dvxFontFmt.c $(SUPPORT_SRCS)
     DVXHLPC_SRCS   = dvxhlpc.c $(SUPPORT_SRCS)
     MKICON_SRCS    = mkicon.c bmpDraw.c
     MKTBICON_SRCS  = mktbicon.c bmpDraw.c
     MKWGTICON_SRCS = mkwgticon.c bmpDraw.c
     BMP2RAW_SRCS   = bmp2raw.c
     MKSOUNDS_SRCS  = mkSounds.c
    -ALL_SRCS       = $(sort $(DVXRES_SRCS) $(DVXHLPC_SRCS) $(MKICON_SRCS) $(MKTBICON_SRCS) $(MKWGTICON_SRCS) $(BMP2RAW_SRCS) $(MKSOUNDS_SRCS))
    +MKFONT_SRCS    = mkfont.c bmpDraw.c $(LIBDVX)/dvxFontFmt.c $(LIBDVX)/dvxPrefs.c $(SUPPORT_SRCS)
    +ALL_SRCS       = $(sort $(DVXRES_SRCS) $(DVXHLPC_SRCS) $(MKICON_SRCS) $(MKTBICON_SRCS) $(MKWGTICON_SRCS) $(BMP2RAW_SRCS) $(MKSOUNDS_SRCS) $(MKFONT_SRCS))
     
     # hostObjs / dosObjs SRCS -> objects for SRCS
     hostObjs = $(patsubst %.c,$(HOBJDIR)/%.o,$(notdir $1))
     dosObjs  = $(patsubst %.c,$(DOBJDIR)/%.o,$(notdir $1))
     
    -HOST_TOOLS = $(HOSTDIR)/dvxres $(HOSTDIR)/mkicon $(HOSTDIR)/mktbicon $(HOSTDIR)/mkwgticon $(HOSTDIR)/bmp2raw $(HOSTDIR)/dvxhlpc $(HOSTDIR)/mksounds
    +HOST_TOOLS = $(HOSTDIR)/dvxres $(HOSTDIR)/mkicon $(HOSTDIR)/mktbicon $(HOSTDIR)/mkwgticon $(HOSTDIR)/bmp2raw $(HOSTDIR)/dvxhlpc $(HOSTDIR)/mksounds $(HOSTDIR)/mkfont
     DOS_TOOLS  = $(SYSTEMDIR)/SPLASH.RAW $(SYSTEMDIR)/DVXHLPC.EXE $(SYSTEMDIR)/DVXRES.EXE
     
     # The default desktop sounds (DVX.INI [sounds]), made by mksounds: one
    @@ -76,10 +80,17 @@ SOUNDDIR    = $(BINDIR)/config/sounds
     SOUND_NAMES = beep error exit info question startup warning winclose winopen
     SOUNDS      = $(patsubst %,$(SOUNDDIR)/%.wav,$(SOUND_NAMES))
     
    +# The fonts: every one the manifest lists, made afresh (so a font taken
    +# out of it leaves no file behind); FONT_STAMP, touched when they are all
    +# made, stands for the set (outside bin/fonts, which is installed whole).
    +FONTDIR      = $(BINDIR)/fonts
    +FONT_LIST    = ../fonts/fonts.ini
    +FONT_STAMP   = ../../obj/tools/fonts.stamp
    +
     ifeq ($(COV),1)
     ALL = $(HOST_TOOLS)
     else
    -ALL = $(HOST_TOOLS) $(DOS_TOOLS) $(SOUNDS)
    +ALL = $(HOST_TOOLS) $(DOS_TOOLS) $(SOUNDS) $(FONT_STAMP)
     endif
     
     .PHONY: all host clean deploy-helpsrc
    @@ -109,9 +120,18 @@ $(HOSTDIR)/bmp2raw: $(call hostObjs,$(BMP2RAW_SRCS)) | $(HOSTDIR)
     $(HOSTDIR)/mksounds: $(call hostObjs,$(MKSOUNDS_SRCS)) | $(HOSTDIR)
     	$(HOSTCC) $(HOSTLDFLAGS) -o $@ $^ -lm
     
    +$(HOSTDIR)/mkfont: $(call hostObjs,$(MKFONT_SRCS)) $(FREETYPE_DIR)/lib/libfreetype.a | $(HOSTDIR)
    +	$(HOSTCC) $(HOSTLDFLAGS) -o $@ $^
    +
     $(SOUNDDIR)/%.wav: $(HOSTDIR)/mksounds | $(SOUNDDIR)
     	$(HOSTDIR)/mksounds $* $@
     
    +$(FONT_STAMP): $(HOSTDIR)/mkfont $(FONT_LIST) $(FONT_SOURCES)/.installed
    +	rm -rf $(FONTDIR) $@
    +	mkdir -p $(dir $@)
    +	$(HOSTDIR)/mkfont $(FONT_LIST) $(FONT_SOURCES) $(FONTDIR)
    +	touch $@
    +
     $(SYSTEMDIR)/SPLASH.RAW: $(HOSTDIR)/bmp2raw ../../assets/splash.bmp | $(SYSTEMDIR)
     	$(HOSTDIR)/bmp2raw ../../assets/splash.bmp $@
     
    @@ -173,5 +193,5 @@ ifeq ($(COV),1)
     	rm -rf $(HOSTDIR) $(HOBJDIR)
     else
     	rm -f $(HOST_TOOLS) $(DOS_TOOLS) $(SOUNDS)
    -	rm -rf $(HOBJDIR) $(DOBJDIR)
    +	rm -rf $(HOBJDIR) $(DOBJDIR) $(FONTDIR) $(FONT_STAMP)
     endif
    diff --git a/src/tools/README.md b/src/tools/README.md
    index aa0323ce..e3d622a2 100644
    --- a/src/tools/README.md
    +++ b/src/tools/README.md
    @@ -45,10 +45,11 @@ dvxres strip 
     | `icon` or `image`  | `DVX_RES_ICON`     | Image data (BMP for icons, PNG/JPEG/GIF for images used by apps). |
     | `text`             | `DVX_RES_TEXT`     | Null-terminated string. Used for metadata: `author`, `copyright`, `publisher`, `description`, `version`, `name`. |
     | `binary`           | `DVX_RES_BINARY`   | Arbitrary binary data, application-defined. |
    +| `font`             | `DVX_RES_FONT`     | A `.DFN` font the program carries (checked to be one); DVX adds it for the program when it loads it. |
     
     For `add` with type `text`, the `` argument is the literal
    -string value. For `icon` or `binary`, the `` argument is a file
    -path; a leading `@` is stripped if present.
    +string value. For `icon`, `binary`, or `font`, the `` argument is
    +a file path; a leading `@` is stripped if present.
     
     ### Manifest File Format (.res)
     
    @@ -85,7 +86,7 @@ longer name is an error, as is a line longer than 1022 characters.
     
     ### Safe Rewrites
     
    -`add`, `build` and `strip` write the new file to a temporary file in
    +`add`, `build`, and `strip` write the new file to a temporary file in
     the same directory and then replace the original, so a failed write
     (for example a full disk) leaves the original untouched. They work on
     a read-only file in a writable directory and keep the file's mode. A
    @@ -169,7 +170,7 @@ dvxhlpc -o  [-i ] [--html ] [--quiet] `, and a write
    @@ -380,6 +381,31 @@ bmp2raw assets/splash.bmp bin/system/SPLASH.RAW
     `bin/host/bmp2raw`
     
     
    +## mkfont -- Font Maker
    +
    +Makes DVX font files (`.DFN`, `libdvx/dvxFontFmt.h`) from TrueType,
    +OpenType, Type 1, BDF, and PCF fonts with FreeType (the copy in
    +`toolchains/freetype`), so DVX only copies bitmaps.
    +
    +```
    +mkfont MANIFEST SOURCEDIR OUTDIR   every font the manifest names
    +mkfont -l MANIFEST SET             the files of a set (Setup's base set)
    +mkfont -p FONT.DFN SHEET.BMP       a picture of a .DFN at every size
    +```
    +
    +The manifest is an INI file: `[fonts]` lists the files to make (8.3
    +base name = family name), `[licences]` names each licence and the
    +texts it is made of, and each font's section gives its style, licence,
    +sources, sizes, hinting, smoothed sizes, gray-cut sizes, and the
    +characters to keep. `mkfont.c`'s header documents every key. Each
    +`.DFN` carries its notice (the family and style, the fonts it was
    +converted from, and their copyright notices) and its licence text.
    +
    +The build makes DVX's fonts (`src/fonts/fonts.ini`) into `bin/fonts`,
    +Setup's (`src/installer/setupFonts.ini`), and those a program carries
    +(an app folder's `fonts.ini`, made into `obj/apps//fonts` before
    +the program is built). The SDK ships it in `bin/`.
    +
     ## bascomp -- DVX BASIC Compiler (host)
     
     Command-line compiler for DVX BASIC projects. Takes a `.dbp` project
    @@ -431,7 +457,7 @@ IDE).
     
     ## helpFiles.sh -- Help Source Discovery
     
    -Finds, deploys and compiles the library and application help sources,
    +Finds, deploys, and compiles the library and application help sources,
     so adding a library or an app never means editing a list. `make
     deploy-helpsrc` runs `helpFiles.sh deploy`; the top-level
     `compile-help` runs `helpFiles.sh libs` (the library part of the system
    @@ -442,7 +468,7 @@ under `bin/`; any other `.dhs` under `src/libs///`
     belongs to `.lib`, and any other under `src/apps///`
     is copied to `bin/apps///` for that directory's own
     `.hcf`. Library help goes into the system help, libdvx's `sysdoc`,
    -`arch` and `apiref` first. App help becomes `X.hlp` next to `X.app`,
    +`arch`, and `apiref` first. App help becomes `X.hlp` next to `X.app`,
     with a generated `X.HCF` for the boot-time rebuild. Deploy removes
     every previously deployed `.DHS` first, so no stale source lingers. A
     source that fits none of these is an error. Widget help is handled
    diff --git a/src/tools/dvxres.c b/src/tools/dvxres.c
    index f9c59b4e..8b9aad90 100644
    --- a/src/tools/dvxres.c
    +++ b/src/tools/dvxres.c
    @@ -28,6 +28,7 @@
     //
     // Commands:
     //   dvxres add      
    +//                (type: icon or image, text, binary, font -- a .DFN, checked)
     //   dvxres build  
     //   dvxres list  
     //   dvxres get     [outfile]
    @@ -39,6 +40,7 @@
     //   [footer with magic + directory offset + count]
     
     #include "dvxResWrite.h"
    +#include "dvxFontFmt.h"
     #include "dvxPlat.h"
     
     #include 
    @@ -51,6 +53,9 @@
     // Longest resource type keyword accepted, plus NUL
     #define TYPE_NAME_MAX     16
     
    +// Why a resource's data could not be had
    +#define LOAD_ERR_MAX      (DFN_ERR_MAX + MANIFEST_LINE_MAX)
    +
     typedef struct {
         const char *name;
         int32_t     type;
    @@ -62,6 +67,7 @@ static const ResTypeNameT sTypeNames[] = {
         { "image",  DVX_RES_ICON   },
         { "text",   DVX_RES_TEXT   },
         { "binary", DVX_RES_BINARY },
    +    { "font",   DVX_RES_FONT   },
     };
     
     #define TYPE_NAME_COUNT ((int32_t)(sizeof(sTypeNames) / sizeof(sTypeNames[0])))
    @@ -76,7 +82,7 @@ static int32_t     cmdGet(const char *dxePath, const char *name, const char *out
     static int32_t     cmdList(const char *dxePath);
     static int32_t     cmdStrip(const char *dxePath);
     static bool        loadLayout(const char *dxePath, DvxResLayoutT *layout);
    -static bool        loadResData(int32_t type, const char *dataArg, uint8_t **outData, uint32_t *outSize);
    +static bool        loadResData(int32_t type, const char *dataArg, uint8_t **outData, uint32_t *outSize, char *err, int32_t errSize);
     static int32_t     parseType(const char *typeStr);
     static const char *typeName(uint32_t type);
     static void        usage(void);
    @@ -86,7 +92,7 @@ static int32_t cmdAdd(const char *dxePath, const char *name, const char *typeStr
         int32_t type = parseType(typeStr);
     
         if (type < 0) {
    -        fprintf(stderr, "Unknown resource type: %s (use icon, text, or binary)\n", typeStr);
    +        fprintf(stderr, "Unknown resource type: %s (use icon, text, binary, or font)\n", typeStr);
             return 1;
         }
     
    @@ -97,14 +103,10 @@ static int32_t cmdAdd(const char *dxePath, const char *name, const char *typeStr
     
         uint8_t *newData = NULL;
         uint32_t newSize = 0;
    +    char     err[LOAD_ERR_MAX];
     
    -    if (!loadResData(type, dataArg, &newData, &newSize)) {
    -        if (type == DVX_RES_TEXT) {
    -            fprintf(stderr, "Out of memory\n");
    -        } else {
    -            fprintf(stderr, "Cannot open or read data file: %s\n", dataArg[0] == '@' ? dataArg + 1 : dataArg);
    -        }
    -
    +    if (!loadResData(type, dataArg, &newData, &newSize, err, sizeof(err))) {
    +        fprintf(stderr, "%s\n", err);
             return 1;
         }
     
    @@ -242,14 +244,10 @@ static int32_t cmdBuild(const char *dxePath, const char *manifestPath) {
     
             uint8_t *newData = NULL;
             uint32_t newSize = 0;
    +        char     err[LOAD_ERR_MAX];
     
    -        if (!loadResData(type, dp, &newData, &newSize)) {
    -            if (type == DVX_RES_TEXT) {
    -                fprintf(stderr, "%s:%ld: out of memory\n", manifestPath, (long)lineNum);
    -            } else {
    -                fprintf(stderr, "%s:%ld: cannot open or read: %s\n", manifestPath, (long)lineNum, dp);
    -            }
    -
    +        if (!loadResData(type, dp, &newData, &newSize, err, sizeof(err))) {
    +            fprintf(stderr, "%s:%ld: %s\n", manifestPath, (long)lineNum, err);
                 goto fail;
             }
     
    @@ -440,12 +438,15 @@ static bool loadLayout(const char *dxePath, DvxResLayoutT *layout) {
     
     // Build a resource payload: text is the string itself plus its NUL; icon
     // and binary data are read from the named file (a leading '@' is optional).
    -static bool loadResData(int32_t type, const char *dataArg, uint8_t **outData, uint32_t *outSize) {
    +// A resource's data: a text's own bytes (with its NUL), any other type's
    +// file (a font's checked to be a DVX font).  False with the reason in err.
    +static bool loadResData(int32_t type, const char *dataArg, uint8_t **outData, uint32_t *outSize, char *err, int32_t errSize) {
         if (type == DVX_RES_TEXT) {
             *outSize = (uint32_t)strlen(dataArg) + 1;
             *outData = (uint8_t *)malloc(*outSize);
     
             if (!*outData) {
    +            snprintf(err, (size_t)errSize, "out of memory");
                 return false;
             }
     
    @@ -458,7 +459,31 @@ static bool loadResData(int32_t type, const char *dataArg, uint8_t **outData, ui
     
         *outData = (uint8_t *)platformReadFile(filePath, &readLen);
         *outSize = (uint32_t)readLen;
    -    return *outData != NULL;
    +
    +    if (!*outData) {
    +        snprintf(err, (size_t)errSize, "cannot open or read %s", filePath);
    +        return false;
    +    }
    +
    +    if (type == DVX_RES_FONT) {
    +        DfnSourceT src;
    +        DfnFileT   f;
    +        char       why[DFN_ERR_MAX];
    +
    +        memset(&src, 0, sizeof(src));
    +        src.name   = filePath;
    +        src.data   = *outData;
    +        src.length = *outSize;
    +
    +        if (!dfnOpen(&src, &f, why, sizeof(why))) {
    +            snprintf(err, (size_t)errSize, "%s", why);
    +            free(*outData);
    +            *outData = NULL;
    +            return false;
    +        }
    +    }
    +
    +    return true;
     }
     
     
    @@ -492,9 +517,9 @@ static void usage(void) {
         fprintf(stderr, "  dvxres list  \n");
         fprintf(stderr, "  dvxres get     [outfile]\n");
         fprintf(stderr, "  dvxres strip \n\n");
    -    fprintf(stderr, "Types: icon (or image), text, binary\n\n");
    +    fprintf(stderr, "Types: icon (or image), text, binary, font (a .DFN, checked)\n\n");
         fprintf(stderr, "For 'add' with type 'text',  is the string value.\n");
    -    fprintf(stderr, "For 'add' with type 'icon' or 'binary',  is a file path.\n\n");
    +    fprintf(stderr, "For 'add' with type 'icon', 'binary', or 'font',  is a file path.\n\n");
         fprintf(stderr, "Manifest format (one per line):\n");
         fprintf(stderr, "  name  type  data\n");
         fprintf(stderr, "  # lines starting with # are comments\n");
    diff --git a/src/tools/mkfont.c b/src/tools/mkfont.c
    new file mode 100644
    index 00000000..c9ed82fd
    --- /dev/null
    +++ b/src/tools/mkfont.c
    @@ -0,0 +1,1288 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// mkfont.c  -- make DVX font files (.DFN, dvxFontFmt.h) on the build host
    +//
    +//   mkfont MANIFEST SOURCEDIR OUTDIR   every font the manifest names
    +//   mkfont -l MANIFEST SET             the files OUTDIR holds for the fonts
    +//                                      of SET (set key below)
    +//   mkfont -p FONT.DFN SHEET.BMP       a picture of a .DFN at every size
    +//
    +// Fonts are rendered here once, with FreeType (the copy pinned in
    +// toolchains/freetype, so the output never changes with the host), 1 bit
    +// per pixel at each size; DVX only copies bitmaps.  A .DFN is a pure
    +// format conversion: a glyph the face lacks is marked missing, never
    +// taken from another face (DVX fills it in when drawing).  Each font's
    +// line on stdout lists the characters it is missing at some size.  Each
    +// file carries its notice text (the family and style, the sources it was
    +// converted from, and the copyright notices they carry) and its licence
    +// text, so the licence goes wherever the font does.
    +//
    +// The manifest (src/fonts/fonts.ini) is an INI file.  [fonts] lists the
    +// files to make, in order: key = the 8.3 base name, value = the family
    +// name people see (at most DFN_FAMILY_MAX - 1 characters).  Each file
    +// has a section of its own:
    +//   style     regular, bold, italic or bolditalic
    +//   licence   a key of [licences]
    +//   boxdraw   true: DVX draws the box, block and shade characters itself
    +//   source    the face, a file under SOURCEDIR; source1, source2 ... more
    +//             faces for other sizes, each with its own keys below ending
    +//             in the same number
    +//   sizes     pixel sizes to make, or "native" for a bitmap face
    +//   hinting   native (the face's own), auto (FreeType's autohinter) or
    +//             none (pixel outlines at their design size)
    +//   graycut   pixel sizes (or "all") whose 1-bit glyphs are cut from the
    +//             anti-aliased rendering, a pixel at least half covered being
    +//             ink, not FreeType's 1-bit rendering (for sizes where the
    +//             face's 1-bit hinting draws a bold face too light)
    +//   encoding  unicode (default) or cp437 (the face numbers its glyphs in
    +//             code page 437)
    +//   weight    design weight for a variable face (its default otherwise)
    +//   smooth    pixel sizes (or "all") also made anti-aliased: a second,
    +//             gray strike of FONT_GRAY_BPP bits per pixel (dvxFontFmt.h)
    +//   set       base: the file is in the base set, Setup's smaller choice
    +//             of fonts (mkfont -l base lists its files for the package)
    +//   chars     the characters to make, as hex codes and ranges ("20-7E
    +//             A9"); the rest are marked missing (default: all 256)
    +// [licences]: key = a name the fonts' licence keys give, value = the
    +// licence files under SOURCEDIR its text is made of.  Licences and
    +// notices are transliterated to ASCII (DOS shows them; DVX's files are
    +// ASCII) and get DOS line ends.
    +// Any error stops the run with a message naming the font or file.
    +
    +#include "dvxFontFmt.h"
    +#include "dvxPlat.h"
    +#include "dvxPrefs.h"
    +#include "dvxTypes.h"
    +#include "bmpDraw.h"
    +#include "thirdparty/stb_ds_wrap.h"
    +
    +#include 
    +#include FT_FREETYPE_H
    +#include FT_MULTIPLE_MASTERS_H
    +#include FT_SFNT_NAMES_H
    +#include FT_TRUETYPE_IDS_H
    +#include FT_BDF_H
    +#include FT_TYPE1_TABLES_H
    +
    +#include 
    +#include 
    +#include 
    +#include 
    +#include 
    +#include 
    +#include 
    +
    +#define SECTION_FONTS     "fonts"
    +#define SECTION_LICENCES  "licences"
    +#define LICENCE_RULE      "------------------------------------------------------------------------"
    +#define SET_BASE          "base"      // the one set (set key)
    +#define WORD_SEP          " "         // appendWord: a list of words
    +#define LIST_SEP          ", "        // appendWord: a list in a sentence
    +#define BDF_COPYRIGHT     "COPYRIGHT"
    +#define SOURCE_MAX        DFN_STRIKE_MAX   // each source makes a size at least
    +#define KEY_MAX           32
    +#define TEXT_MAX          1024
    +#define NAME_83_MAX       8           // base name of an 8.3 file
    +#define DOS_EOL           "\r\n"
    +#define FT_SHIFT          6           // 26.6 fixed point
    +#define FT_HALF           32
    +#define FIXED_SHIFT       16          // 16.16 fixed point
    +#define AXIS_WEIGHT       FT_MAKE_TAG('w', 'g', 'h', 't')
    +#define CHAR_SPACE        0x20
    +#define GRAY_IN_MAX       255         // FreeType's gray bitmaps: 0 .. 255
    +#define GRAY_IN_ROUND     127
    +#define GRAY_IN_HALF      128         // a gray-cut size: ink from half covered
    +#define BITS_PER_BYTE     8
    +#define MONO_LEFT_BIT     0x80        // a 1-bit row byte's leftmost pixel
    +#define UNI_SPACE         0x0020      // the characters a face draws as nothing
    +#define UNI_NBSP          0x00A0
    +#define PIXEL_SIZE_MAX    INT8_MAX    // a glyph's top must fit FontGlyphT.top
    +#define CHAR_LAST         (DFN_CHAR_COUNT - 1)
    +#define HEX_BASE          16
    +
    +// UTF-8 (licence texts)
    +#define ASCII_MAX         0x7F
    +#define UTF8_LEAD2        0xC0        // 110xxxxx: 1 continuation byte
    +#define UTF8_LEAD3        0xE0        // 1110xxxx: 2
    +#define UTF8_LEAD4        0xF0        // 11110xxx: 3
    +#define UTF8_CONT_MASK    0xC0
    +#define UTF8_CONT         0x80        // 10xxxxxx
    +#define UTF8_CONT_BITS    0x3F
    +#define UTF8_CONT_SHIFT   6
    +
    +// Preview sheet layout and colours
    +#define SHEET_MARGIN      8
    +#define SHEET_GAP         6
    +#define SHEET_GRID_COLS   16
    +#define SHEET_CELL_PAD    2
    +#define SHEET_SAMPLE      "The quick brown fox jumps over the lazy dog.  0123456789 !?&$%"
    +#define SHEET_BG          255
    +#define SHEET_INK         0
    +#define SHEET_MISS_R      220
    +#define SHEET_MISS_G      40
    +#define SHEET_MISS_B      40
    +#define SHEET_RULE        200
    +
    +typedef enum {
    +    HintNativeE,
    +    HintAutoE,
    +    HintNoneE
    +} HintE;
    +
    +typedef struct {
    +    char    path[DVX_MAX_PATH];      // under SOURCEDIR
    +    int32_t sizes[DFN_STRIKE_MAX];
    +    int32_t sizeCount;               // 0: the bitmap face's own size
    +    int32_t smooth[DFN_STRIKE_MAX];  // sizes also made gray
    +    int32_t smoothCount;
    +    int32_t grayCut[DFN_STRIKE_MAX]; // sizes whose 1-bit glyphs are cut from gray
    +    int32_t grayCutCount;
    +    int32_t hinting;                 // HintE
    +    int32_t cp437;                   // the face numbers its glyphs in code page 437
    +    int32_t weight;                  // 0: the face's default
    +    const bool *keep;                // [DFN_CHAR_COUNT]: the characters to make (the font's chars key)
    +} SourceT;
    +
    +typedef struct {
    +    DfnStrikeT info;
    +    FontGlyphT glyphs[DFN_CHAR_COUNT];
    +    uint8_t   *bitmaps;             // stb_ds
    +} StrikeT;
    +
    +typedef struct {
    +    uint32_t    codepoint;
    +    const char *ascii;
    +} TranslitT;
    +
    +// A [licences] key and its text (ASCII, DOS line ends)
    +typedef struct {
    +    const char *key;
    +    char       *text;               // stb_ds
    +} LicenceT;
    +
    +// What a licence or notice may hold beyond ASCII, and its ASCII form.
    +// Any other character stops the run (so a new text is looked at).
    +static const TranslitT sTranslit[] = {
    +    { 0x00A9, "(C)" },
    +    { 0x00C6, "AE"  },
    +    { 0x2013, "-"   },
    +    { 0x201C, "\""  },
    +    { 0x201D, "\""  }
    +};
    +
    +// Manifest vocabularies (label: the README's spelling)
    +static const PrefsChoiceT sStyles[] = {
    +    { "Regular",     "regular",    0                                 },
    +    { "Bold",        "bold",       FONT_STYLE_BOLD                    },
    +    { "Italic",      "italic",     FONT_STYLE_ITALIC                  },
    +    { "Bold Italic", "bolditalic", FONT_STYLE_BOLD | FONT_STYLE_ITALIC }
    +};
    +
    +static const PrefsChoiceT sHinting[] = {
    +    { "Native", "native", HintNativeE },
    +    { "Auto",   "auto",   HintAutoE   },
    +    { "None",   "none",   HintNoneE   }
    +};
    +
    +static const PrefsChoiceT sEncodings[] = {
    +    { "Unicode",       "unicode", false },
    +    { "Code page 437", "cp437",   true  }
    +};
    +
    +static FT_Library sLib;
    +static LicenceT  *sLicences;        // stb_ds
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +static void        appendAscii(char **text, uint32_t cp, const char *where, int32_t line);
    +static void        appendDosLines(char **out, const char *text, int32_t len);
    +static void        appendUtf8(char **text, const char *utf8, int32_t len, const char *where);
    +static void        appendWord(char *buf, size_t size, const char *word, const char *sep);
    +static void        buildFont(PrefsHandleT *m, const char *name, const char *srcDir, const char *outDir);
    +static void        check83(const char *what, const char *name);
    +static void        die(const char *fmt, ...) __attribute__((format(printf, 1, 2), noreturn));
    +static int32_t     drawGlyph(BmpCanvasT *c, const DfnStrikeDataT *s, int32_t ch, int32_t x, int32_t baseline);
    +static char       *faceNotice(const char *path, FT_Face face);
    +static int32_t     getChoice(PrefsHandleT *m, const char *section, const char *key, const PrefsChoiceT *choices, int32_t count, const char *defaultName);
    +static bool        hasSize(const int32_t *sizes, int32_t count, int32_t size);
    +static bool        isFixed(const StrikeT *strikes, int32_t count);
    +static const char *licenceText(const char *key);
    +static int         listSet(const char *manifest, const char *set);
    +static void        loadLicences(PrefsHandleT *m, const char *srcDir);
    +static bool        packBitmap(const FT_Bitmap *bm, bool gray, uint8_t **out);
    +static int32_t     parseSizes(const char *name, const char *text, int32_t *sizes, int32_t max);
    +static int32_t     penMove(const DfnStrikeDataT *s, int32_t ch);
    +static int32_t     preview(const char *dfnPath, const char *bmpPath);
    +static void        printMissing(const char *name, const StrikeT *strikes, int32_t count);
    +static void        readChars(PrefsHandleT *m, const char *name, bool *keep);
    +static int32_t     readSizeList(PrefsHandleT *m, const char *name, const char *key, const char *what, const SourceT *src, int32_t *list);
    +static bool        readSource(PrefsHandleT *m, const char *name, int32_t n, SourceT *out);
    +static char       *renderSource(const char *name, const char *srcDir, const SourceT *src, StrikeT **strikes);
    +static void        renderStrike(const char *name, FT_Face face, const SourceT *src, int32_t pixelSize, bool gray, StrikeT *out);
    +static void        setWeight(const char *name, const char *path, FT_Face face, int32_t weight);
    +static int         strikeCompare(const void *a, const void *b);
    +static const char *translit(uint32_t cp);
    +static void        writeFont(const char *path, DfnHeaderT *h, StrikeT *strikes, const char *notice, const char *licence);
    +int                main(int argc, char **argv);
    +
    +
    +// Character cp of a text (where, line: its file and line, for the error)
    +// onto the stb_ds text in its ASCII form; a carriage return is dropped
    +static void appendAscii(char **text, uint32_t cp, const char *where, int32_t line) {
    +    if (cp == '\r') {
    +        return;
    +    }
    +
    +    if (cp <= ASCII_MAX) {
    +        arrput(*text, (char)cp);
    +        return;
    +    }
    +
    +    const char *ascii = translit(cp);
    +
    +    if (!ascii) {
    +        die("%s:%" PRId32 ": no ASCII form for U+%04" PRIX32 " (add one to sTranslit)", where, line, cp);
    +    }
    +
    +    for (; *ascii; ascii++) {
    +        arrput(*text, *ascii);
    +    }
    +}
    +
    +
    +// The UTF-8 text utf8 (len bytes; where: its file, for errors) onto the
    +// stb_ds text in ASCII (appendAscii)
    +// The lines of text (len bytes, '\n' between lines) onto *out (stb_ds)
    +// with DOS line ends; a final line end ends the last line, it does not
    +// start another
    +static void appendDosLines(char **out, const char *text, int32_t len) {
    +    int32_t start = 0;
    +
    +    while (start < len) {
    +        const char *eol = (const char *)memchr(text + start, '\n', (size_t)(len - start));
    +        int32_t     end = eol ? (int32_t)(eol - text) : len;
    +
    +        memcpy(arraddnptr(*out, end - start), text + start, (size_t)(end - start));
    +        memcpy(arraddnptr(*out, (int32_t)strlen(DOS_EOL)), DOS_EOL, strlen(DOS_EOL));
    +        start = end + 1;
    +    }
    +}
    +
    +
    +static void appendUtf8(char **text, const char *utf8, int32_t len, const char *where) {
    +    int32_t line = 1;
    +
    +    for (int32_t i = 0; i < len; i++) {
    +        uint8_t  b    = (uint8_t)utf8[i];
    +        uint32_t cp   = b;
    +        int32_t  more = 0;
    +
    +        // A lead byte and its 1 to 3 continuation bytes
    +        if (b > ASCII_MAX) {
    +            bool valid;
    +
    +            more  = (b >= UTF8_LEAD4) ? 3 : (b >= UTF8_LEAD3) ? 2 : (b >= UTF8_LEAD2) ? 1 : 0;
    +            cp    = b & (UTF8_CONT_BITS >> more);
    +            valid = more > 0 && i + more < len;
    +
    +            for (int32_t k = 1; k <= more && valid; k++) {
    +                uint8_t cont = (uint8_t)utf8[i + k];
    +
    +                valid = (cont & UTF8_CONT_MASK) == UTF8_CONT;
    +                cp    = (cp << UTF8_CONT_SHIFT) | (cont & UTF8_CONT_BITS);
    +            }
    +
    +            if (!valid) {
    +                die("%s:%" PRId32 ": is not UTF-8", where, line);
    +            }
    +        }
    +
    +        appendAscii(text, cp, where, line);
    +        line += (cp == '\n');
    +        i    += more;
    +    }
    +}
    +
    +
    +// word onto the list in buf, after sep when the list has words already
    +static void appendWord(char *buf, size_t size, const char *word, const char *sep) {
    +    size_t len = strlen(buf);
    +
    +    snprintf(buf + len, size - len, "%s%s", len ? sep : "", word);
    +}
    +
    +
    +static void buildFont(PrefsHandleT *m, const char *name, const char *srcDir, const char *outDir) {
    +    DfnHeaderT  h;
    +    StrikeT    *strikes            = NULL;
    +    char      **notices            = NULL;
    +    char       *notice             = NULL;     // stb_ds
    +    const char *family             = prefsGetString(m, SECTION_FONTS, name, "");
    +    const char *licence            = prefsGetString(m, name, "licence", "");
    +    char        fromList[TEXT_MAX] = "";
    +    char        line[TEXT_MAX];
    +    char        path[DVX_MAX_PATH];
    +    bool        keep[DFN_CHAR_COUNT];
    +
    +    if (!family[0] || strlen(family) >= DFN_FAMILY_MAX) {
    +        die("%s: its family name is missing or longer than %d characters", name, DFN_FAMILY_MAX - 1);
    +    }
    +
    +    if (!licenceText(licence)) {
    +        die("%s: licence '%s' is not in [%s]", name, licence, SECTION_LICENCES);
    +    }
    +
    +    const char *set = prefsGetString(m, name, "set", "");
    +
    +    if (set[0] && strcasecmp(set, SET_BASE) != 0) {
    +        die("%s: set '%s' is not %s", name, set, SET_BASE);
    +    }
    +
    +    readChars(m, name, keep);
    +    memset(&h, 0, sizeof(h));
    +    memcpy(h.magic, DFN_MAGIC, DFN_MAGIC_LEN);
    +    h.version = DFN_VERSION;
    +    h.style   = (uint16_t)getChoice(m, name, "style", sStyles, DVX_ARRAY_LEN(sStyles), "regular");
    +    snprintf(h.family, sizeof(h.family), "%s", family);
    +
    +    if (prefsGetBool(m, name, "boxdraw", false)) {
    +        h.faceFlags |= DFN_FACE_BOXDRAW;
    +    }
    +
    +    for (int32_t n = 0; n < SOURCE_MAX; n++) {
    +        SourceT src;
    +
    +        if (!readSource(m, name, n, &src)) {
    +            continue;
    +        }
    +
    +        src.keep = keep;
    +
    +        char *text  = renderSource(name, srcDir, &src, &strikes);
    +        bool  known = !text[0];
    +
    +        for (int32_t i = 0; i < arrlen(notices) && !known; i++) {
    +            known = strcmp(notices[i], text) == 0;
    +        }
    +
    +        if (known) {
    +            free(text);
    +        } else {
    +            arrput(notices, text);
    +        }
    +
    +        appendWord(fromList, sizeof(fromList), platformPathBaseName(src.path), LIST_SEP);
    +    }
    +
    +    int32_t count = (int32_t)arrlen(strikes);
    +
    +    if (count == 0 || count > DFN_STRIKE_MAX) {
    +        die("%s: %" PRId32 " sizes (1 to %d allowed)", name, count, DFN_STRIKE_MAX);
    +    }
    +
    +    qsort(strikes, (size_t)count, sizeof(strikes[0]), strikeCompare);
    +
    +    for (int32_t i = 1; i < count; i++) {
    +        if (strikes[i].info.pixelSize == strikes[i - 1].info.pixelSize && strikes[i].info.flags == strikes[i - 1].info.flags) {
    +            die("%s: two of its sources make %" PRId32 " px", name, (int32_t)strikes[i].info.pixelSize);
    +        }
    +    }
    +
    +    if (isFixed(strikes, count)) {
    +        h.faceFlags |= DFN_FACE_FIXED;
    +    }
    +
    +    // The notice text: the face, what it was converted from, and the
    +    // copyright notices of its sources
    +    snprintf(line, sizeof(line), "%s %s, converted for DVX from %s.", family, sStyles[prefsChoiceIndexOfValue(sStyles, DVX_ARRAY_LEN(sStyles), h.style)].label, fromList);
    +    appendDosLines(¬ice, line, (int32_t)strlen(line));
    +
    +    for (int32_t i = 0; i < arrlen(notices); i++) {
    +        appendDosLines(¬ice, notices[i], (int32_t)strlen(notices[i]));
    +        free(notices[i]);
    +    }
    +
    +    arrput(notice, '\0');
    +    h.strikeCount = (uint16_t)count;
    +    snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s" DFN_EXT, outDir, name);
    +    writeFont(path, &h, strikes, notice, licenceText(licence));
    +    printMissing(name, strikes, count);
    +
    +    for (int32_t i = 0; i < count; i++) {
    +        arrfree(strikes[i].bitmaps);
    +    }
    +
    +    arrfree(notice);
    +    arrfree(notices);
    +    arrfree(strikes);
    +}
    +
    +
    +// Stop unless name is an 8.3 base name as DVX writes them: 1 to 8
    +// capitals, digits, '_' or '-'
    +static void check83(const char *what, const char *name) {
    +    size_t len = strlen(name);
    +    bool   ok  = len >= 1 && len <= NAME_83_MAX;
    +
    +    for (size_t i = 0; i < len && ok; i++) {
    +        ok = isupper((unsigned char)name[i]) || isdigit((unsigned char)name[i]) || name[i] == '_' || name[i] == '-';
    +    }
    +
    +    if (!ok) {
    +        die("%s name '%s' is not 1 to %d capitals, digits, '_' or '-'", what, name, NAME_83_MAX);
    +    }
    +}
    +
    +
    +static void die(const char *fmt, ...) {
    +    va_list ap;
    +
    +    fputs("mkfont: ", stderr);
    +    va_start(ap, fmt);
    +    vfprintf(stderr, fmt, ap);
    +    va_end(ap);
    +    fputc('\n', stderr);
    +    exit(1);
    +}
    +
    +
    +// Character ch of strike s with its pen at x on baseline, a missing one
    +// as a red box (penMove wide).  Returns the pen movement.
    +static int32_t drawGlyph(BmpCanvasT *c, const DfnStrikeDataT *s, int32_t ch, int32_t x, int32_t baseline) {
    +    const FontGlyphT *g = &s->glyphs[ch];
    +
    +    if (g->flags & FONT_GLYPH_MISSING) {
    +        bmpFillRect(c, x, baseline - s->info.ascent, penMove(s, ch), s->info.ascent, SHEET_MISS_R, SHEET_MISS_G, SHEET_MISS_B);
    +        return penMove(s, ch);
    +    }
    +
    +    const uint8_t *rows    = s->bitmaps + s->bitmapAt[ch];
    +    bool           gray    = (s->info.flags & DFN_STRIKE_GRAY2) != 0;
    +    int32_t        rowSize = FONT_ROW_BYTES_BPP(g->width, gray ? FONT_GRAY_BPP : 1);
    +
    +    // A gray pixel's level shades from the paper to the ink
    +    for (int32_t y = 0; y < g->height; y++) {
    +        const uint8_t *row = rows + y * rowSize;
    +
    +        for (int32_t xx = 0; xx < g->width; xx++) {
    +            int32_t level = gray ? FONT_GRAY_LEVEL(row, xx) : ((row[FONT_PIXEL_BYTE(xx)] & FONT_PIXEL_BIT(xx)) ? FONT_GRAY_FULL : 0);
    +            uint8_t shade = (uint8_t)(SHEET_BG - (SHEET_BG - SHEET_INK) * level / FONT_GRAY_FULL);
    +
    +            if (level > 0) {
    +                bmpPixel(c, x + g->left + xx, baseline - g->top + y, shade, shade, shade);
    +            }
    +        }
    +    }
    +
    +    return g->advance;
    +}
    +
    +
    +// The copyright notice face (the file at path) carries, in ASCII, its
    +// trailing blanks and line ends dropped: the sfnt name table's (Windows
    +// Unicode), a BDF / PCF COPYRIGHT property or a Type 1 font's notice
    +// (UTF-8); "" when it has none.  For free.
    +static char *faceNotice(const char *path, FT_Face face) {
    +    char           *text = NULL;    // stb_ds
    +    const char     *utf8 = NULL;    // a BDF or Type 1 notice
    +    BDF_PropertyRec prop;
    +    PS_FontInfoRec  info;
    +
    +    for (FT_UInt i = 0; i < FT_Get_Sfnt_Name_Count(face) && !text; i++) {
    +        FT_SfntName sn;
    +
    +        if (FT_Get_Sfnt_Name(face, i, &sn) != 0 || sn.name_id != TT_NAME_ID_COPYRIGHT || sn.platform_id != TT_PLATFORM_MICROSOFT || sn.encoding_id != TT_MS_ID_UNICODE_CS) {
    +            continue;
    +        }
    +
    +        // UTF-16, big-endian
    +        for (FT_UInt k = 0; k + 1 < sn.string_len; k += 2) {
    +            appendAscii(&text, ((uint32_t)sn.string[k] << 8) | sn.string[k + 1], path, 1);
    +        }
    +    }
    +
    +    if (!text && FT_Get_BDF_Property(face, BDF_COPYRIGHT, &prop) == 0 && prop.type == BDF_PROPERTY_TYPE_ATOM) {
    +        utf8 = prop.u.atom;
    +    }
    +
    +    if (!text && !utf8 && FT_Get_PS_Font_Info(face, &info) == 0) {
    +        utf8 = info.notice;
    +    }
    +
    +    if (utf8) {
    +        appendUtf8(&text, utf8, (int32_t)strlen(utf8), path);
    +    }
    +
    +    int32_t len = (int32_t)arrlen(text);
    +
    +    while (len > 0 && isspace((unsigned char)text[len - 1])) {
    +        len--;
    +    }
    +
    +    arrsetlen(text, len);
    +    arrput(text, '\0');
    +
    +    char *notice = strdup(text);
    +
    +    arrfree(text);
    +
    +    if (!notice) {
    +        die("out of memory");
    +    }
    +
    +    return notice;
    +}
    +
    +
    +// The value of the choice key names in section (defaultName when the key
    +// is absent); stops on a name the choices do not have
    +static int32_t getChoice(PrefsHandleT *m, const char *section, const char *key, const PrefsChoiceT *choices, int32_t count, const char *defaultName) {
    +    const char *text  = prefsGetString(m, section, key, defaultName);
    +    int32_t     index = prefsChoiceIndex(choices, count, text);
    +
    +    if (index < 0) {
    +        char names[TEXT_MAX] = "";
    +
    +        for (int32_t i = 0; i < count; i++) {
    +            appendWord(names, sizeof(names), choices[i].iniName, WORD_SEP);
    +        }
    +
    +        die("%s: %s '%s' is not one of: %s", section, key, text, names);
    +    }
    +
    +    return choices[index].value;
    +}
    +
    +
    +// Every glyph present at a size moves the pen as far as its space does
    +// Whether size is one of the count sizes
    +static bool hasSize(const int32_t *sizes, int32_t count, int32_t size) {
    +    for (int32_t i = 0; i < count; i++) {
    +        if (sizes[i] == size) {
    +            return true;
    +        }
    +    }
    +
    +    return false;
    +}
    +
    +
    +static bool isFixed(const StrikeT *strikes, int32_t count) {
    +    for (int32_t i = 0; i < count; i++) {
    +        int32_t pitch = strikes[i].glyphs[CHAR_SPACE].advance;
    +
    +        for (int32_t c = 0; c < DFN_CHAR_COUNT; c++) {
    +            const FontGlyphT *g = &strikes[i].glyphs[c];
    +
    +            if (!(g->flags & FONT_GLYPH_MISSING) && g->advance != pitch) {
    +                return false;
    +            }
    +        }
    +    }
    +
    +    return true;
    +}
    +
    +
    +// mkfont -l: the files OUTDIR holds for the fonts whose set is set, one
    +// per line relative to OUTDIR with '/' (the package tool takes the list
    +// as a tier, src/installer/mkDist.sh): the licence folder and its index,
    +// each font, and each licence they use, once.
    +// The licence text of [licences] key key; NULL for a key there is none of
    +static const char *licenceText(const char *key) {
    +    for (int32_t i = 0; i < arrlen(sLicences); i++) {
    +        if (strcasecmp(sLicences[i].key, key) == 0) {
    +            return sLicences[i].text;
    +        }
    +    }
    +
    +    return NULL;
    +}
    +
    +
    +static int listSet(const char *manifest, const char *set) {
    +    PrefsHandleT *m = prefsLoad(manifest);
    +
    +    if (!m || !prefsKeyName(m, SECTION_FONTS, 0)) {
    +        die("%s has no [%s] list", manifest, SECTION_FONTS);
    +    }
    +
    +    for (int32_t i = 0; prefsKeyName(m, SECTION_FONTS, i); i++) {
    +        const char *name = prefsKeyName(m, SECTION_FONTS, i);
    +
    +        if (strcasecmp(prefsGetString(m, name, "set", ""), set) == 0) {
    +            printf("%s" DFN_EXT "\n", name);
    +        }
    +    }
    +
    +    prefsClose(m);
    +    return 0;
    +}
    +
    +
    +// The text of every [licences] key, each made of its files (all read
    +// here, so a bad licence stops the run whether a font uses it or not)
    +static void loadLicences(PrefsHandleT *m, const char *srcDir) {
    +    for (int32_t i = 0; prefsKeyName(m, SECTION_LICENCES, i); i++) {
    +        LicenceT lic;
    +        char     files[TEXT_MAX];
    +        bool     first = true;
    +
    +        lic.key  = prefsKeyName(m, SECTION_LICENCES, i);
    +        lic.text = NULL;
    +        snprintf(files, sizeof(files), "%s", prefsGetString(m, SECTION_LICENCES, lic.key, ""));
    +
    +        for (char *f = strtok(files, " \t"); f; f = strtok(NULL, " \t")) {
    +            char    src[DVX_MAX_PATH];
    +            char   *ascii = NULL;    // stb_ds
    +            int32_t len   = 0;
    +
    +            if (!first) {
    +                appendDosLines(&lic.text, "\n" LICENCE_RULE "\n\n", (int32_t)strlen("\n" LICENCE_RULE "\n\n"));
    +            }
    +
    +            first = false;
    +            snprintf(src, sizeof(src), "%s" DVX_PATH_SEP "%s", srcDir, f);
    +
    +            char *utf8 = platformReadFile(src, &len);
    +
    +            if (!utf8) {
    +                die("cannot read %s", src);
    +            }
    +
    +            appendUtf8(&ascii, utf8, len, src);
    +            appendDosLines(&lic.text, ascii, (int32_t)arrlen(ascii));
    +            arrfree(ascii);
    +            free(utf8);
    +        }
    +
    +        if (first) {
    +            die("[%s] %s names no file", SECTION_LICENCES, lic.key);
    +        }
    +
    +        arrput(lic.text, '\0');
    +        arrput(sLicences, lic);
    +    }
    +}
    +
    +
    +// FreeType bitmap bm into *out (an stb_ds array, emptied first) as DVX
    +// stores it: a 1-bit bitmap's rows as they are; a gray one's 0 .. 255
    +// pixels in FONT_GRAY_FULL + 1 levels, FONT_GRAY_BPP bits each (gray), or
    +// at 1 bit, a pixel at least half covered set (a gray-cut size).  Returns
    +// whether any pixel is set.
    +static bool packBitmap(const FT_Bitmap *bm, bool gray, uint8_t **out) {
    +    int32_t w        = (int32_t)bm->width;
    +    int32_t rowBytes = FONT_ROW_BYTES_BPP(w, gray ? FONT_GRAY_BPP : 1);
    +    bool    ink      = false;
    +
    +    arrsetlen(*out, rowBytes * (int32_t)bm->rows);
    +
    +    // An empty bitmap (a space's): no rows to pack
    +    if (arrlen(*out) == 0) {
    +        return false;
    +    }
    +
    +    memset(*out, 0, (size_t)arrlen(*out));
    +
    +    for (int32_t y = 0; y < (int32_t)bm->rows; y++) {
    +        const uint8_t *src = bm->buffer + y * bm->pitch;
    +        uint8_t       *dst = *out + y * rowBytes;
    +
    +        if (bm->pixel_mode == FT_PIXEL_MODE_MONO) {
    +            memcpy(dst, src, (size_t)rowBytes);
    +        } else if (gray) {
    +            for (int32_t x = 0; x < w; x++) {
    +                uint8_t level = (uint8_t)((src[x] * FONT_GRAY_FULL + GRAY_IN_ROUND) / GRAY_IN_MAX);
    +
    +                dst[x / FONT_GRAY_PER_BYTE] |= (uint8_t)(level << FONT_GRAY_SHIFT(x));
    +            }
    +        } else {
    +            for (int32_t x = 0; x < w; x++) {
    +                if (src[x] >= GRAY_IN_HALF) {
    +                    dst[x / BITS_PER_BYTE] |= (uint8_t)(MONO_LEFT_BIT >> (x % BITS_PER_BYTE));
    +                }
    +            }
    +        }
    +
    +        for (int32_t b = 0; b < rowBytes && !ink; b++) {
    +            ink = dst[b] != 0;
    +        }
    +    }
    +
    +    return ink;
    +}
    +
    +
    +// Pixel sizes in text ("8 9 10"); 0 for "native"
    +static int32_t parseSizes(const char *name, const char *text, int32_t *sizes, int32_t max) {
    +    int32_t count = 0;
    +    char    buf[TEXT_MAX];
    +
    +    if (strcasecmp(text, "native") == 0) {
    +        return 0;
    +    }
    +
    +    snprintf(buf, sizeof(buf), "%s", text);
    +
    +    for (char *t = strtok(buf, " \t,"); t; t = strtok(NULL, " \t,")) {
    +        char *end  = NULL;
    +        long  size = strtol(t, &end, 10);
    +
    +        if (*end || size < 1 || size > PIXEL_SIZE_MAX || count == max) {
    +            die("%s: size '%s' is not a pixel size (or there are over %" PRId32 ")", name, t, max);
    +        }
    +
    +        sizes[count++] = (int32_t)size;
    +    }
    +
    +    if (count == 0) {
    +        die("%s: no sizes", name);
    +    }
    +
    +    return count;
    +}
    +
    +
    +// The pen movement of character ch of strike s on a sheet: its advance,
    +// a space's for a missing one (drawn as a box)
    +static int32_t penMove(const DfnStrikeDataT *s, int32_t ch) {
    +    return (s->glyphs[ch].flags & FONT_GLYPH_MISSING) ? s->glyphs[CHAR_SPACE].advance : s->glyphs[ch].advance;
    +}
    +
    +
    +// A picture of a .DFN: per size, a sample line and the 256 characters
    +// in a 16 x 16 grid, missing ones as red boxes
    +static int32_t preview(const char *dfnPath, const char *bmpPath) {
    +    DfnSourceT     src;
    +    DfnFileT       f;
    +    DfnStrikeDataT s[DFN_STRIKE_MAX];
    +    char           sample[DFN_STRIKE_MAX][TEXT_MAX];
    +    char           err[DFN_ERR_MAX];
    +    int32_t        width  = 0;
    +    int32_t        height = SHEET_MARGIN;
    +
    +    dfnSourceFile(&src, dfnPath);
    +
    +    if (!dfnOpen(&src, &f, err, sizeof(err))) {
    +        die("%s", err);
    +    }
    +
    +    for (int32_t i = 0; i < f.header.strikeCount; i++) {
    +        if (!dfnLoadStrike(&src, &f, i, &s[i], err, sizeof(err))) {
    +            die("%s", err);
    +        }
    +
    +        int32_t sampleW = 0;
    +        int32_t cellW   = s[i].info.maxAdvance + SHEET_CELL_PAD;
    +
    +        snprintf(sample[i], sizeof(sample[i]), "%" PRId32 " px%s: %s", (int32_t)s[i].info.pixelSize, (s[i].info.flags & DFN_STRIKE_GRAY2) ? " smoothed" : "", SHEET_SAMPLE);
    +
    +        for (const char *p = sample[i]; *p; p++) {
    +            sampleW += penMove(&s[i], (uint8_t)*p);
    +        }
    +
    +        width   = DVX_MAX(width, DVX_MAX(sampleW, SHEET_GRID_COLS * cellW));
    +        height += s[i].info.lineHeight * (1 + DFN_CHAR_COUNT / SHEET_GRID_COLS) + SHEET_GAP * 2;
    +    }
    +
    +    BmpCanvasT c;
    +
    +    if (!bmpInit(&c, width + SHEET_MARGIN * 2, height + SHEET_MARGIN)) {
    +        die("out of memory");
    +    }
    +
    +    bmpClear(&c, SHEET_BG, SHEET_BG, SHEET_BG);
    +
    +    int32_t y = SHEET_MARGIN;
    +
    +    for (int32_t i = 0; i < f.header.strikeCount; i++) {
    +        int32_t lineH = s[i].info.lineHeight;
    +        int32_t cellW = s[i].info.maxAdvance + SHEET_CELL_PAD;
    +        int32_t x     = SHEET_MARGIN;
    +
    +        for (const char *p = sample[i]; *p; p++) {
    +            x += drawGlyph(&c, &s[i], (uint8_t)*p, x, y + s[i].info.ascent);
    +        }
    +
    +        y += lineH + SHEET_GAP;
    +
    +        for (int32_t ch = 0; ch < DFN_CHAR_COUNT; ch++) {
    +            int32_t cx = SHEET_MARGIN + (ch % SHEET_GRID_COLS) * cellW;
    +            int32_t cy = y + (ch / SHEET_GRID_COLS) * lineH;
    +
    +            drawGlyph(&c, &s[i], ch, cx, cy + s[i].info.ascent);
    +        }
    +
    +        y += lineH * (DFN_CHAR_COUNT / SHEET_GRID_COLS) + SHEET_GAP;
    +        bmpHLine(&c, SHEET_MARGIN, y - SHEET_GAP / 2, width, SHEET_RULE, SHEET_RULE, SHEET_RULE);
    +        dfnFreeStrike(&s[i]);
    +    }
    +
    +    bool ok = bmpWrite(&c, bmpPath);
    +
    +    bmpFree(&c);
    +    return ok ? 0 : 1;
    +}
    +
    +
    +// The font's line: its sizes, how many are smoothed too, and the
    +// characters it is missing at some size, as code ranges ("01-1F 7F")
    +static void printMissing(const char *name, const StrikeT *strikes, int32_t count) {
    +    char    list[TEXT_MAX] = "";
    +    int32_t total          = 0;
    +    int32_t runStart       = -1;
    +
    +    // One past the last character closes a run that reaches it
    +    for (int32_t c = 0; c <= DFN_CHAR_COUNT; c++) {
    +        bool missing = false;
    +
    +        for (int32_t i = 0; i < count && c < DFN_CHAR_COUNT && !missing; i++) {
    +            missing = (strikes[i].glyphs[c].flags & FONT_GLYPH_MISSING) != 0;
    +        }
    +
    +        if (missing) {
    +            total++;
    +            runStart = (runStart < 0) ? c : runStart;
    +            continue;
    +        }
    +
    +        if (runStart >= 0) {
    +            char range[KEY_MAX];
    +
    +            if (runStart == c - 1) {
    +                snprintf(range, sizeof(range), "%02" PRIX32, (uint32_t)runStart);
    +            } else {
    +                snprintf(range, sizeof(range), "%02" PRIX32 "-%02" PRIX32, (uint32_t)runStart, (uint32_t)(c - 1));
    +            }
    +
    +            appendWord(list, sizeof(list), range, WORD_SEP);
    +            runStart = -1;
    +        }
    +    }
    +
    +    int32_t smoothed = 0;
    +
    +    for (int32_t i = 0; i < count; i++) {
    +        smoothed += (strikes[i].info.flags & DFN_STRIKE_GRAY2) ? 1 : 0;
    +    }
    +
    +    printf("%-8s %2" PRId32 " sizes, %2" PRId32 " smoothed, missing %3" PRId32 ": %s\n", name, count - smoothed, smoothed, total, list);
    +}
    +
    +
    +// The characters font name makes into keep: those its chars key lists
    +// (hex codes and ranges, "20-7E A9"), or all of them
    +static void readChars(PrefsHandleT *m, const char *name, bool *keep) {
    +    const char *text = prefsGetString(m, name, "chars", NULL);
    +    char        buf[TEXT_MAX];
    +
    +    memset(keep, !text, sizeof(bool) * DFN_CHAR_COUNT);
    +
    +    if (!text) {
    +        return;
    +    }
    +
    +    snprintf(buf, sizeof(buf), "%s", text);
    +
    +    for (char *t = strtok(buf, " \t,"); t; t = strtok(NULL, " \t,")) {
    +        char *end = NULL;
    +        long  lo  = strtol(t, &end, HEX_BASE);
    +        long  hi  = lo;
    +
    +        if (end != t && *end == '-') {
    +            char *from = end + 1;
    +
    +            hi = strtol(from, &end, HEX_BASE);
    +            end = (end == from) ? t : end;
    +        }
    +
    +        if (end == t || *end || lo < 0 || hi > CHAR_LAST || lo > hi) {
    +            die("%s: chars '%s' is not a hex code or range from 00 to %X", name, t, CHAR_LAST);
    +        }
    +
    +        for (long c = lo; c <= hi; c++) {
    +            keep[c] = true;
    +        }
    +    }
    +}
    +
    +
    +// The sizes font name's key lists (or "all": every size of src) into
    +// list, what naming them in messages; how many.  Each must be one of
    +// src's sizes, and a bitmap face (its glyphs its own) takes none.
    +static int32_t readSizeList(PrefsHandleT *m, const char *name, const char *key, const char *what, const SourceT *src, int32_t *list) {
    +    const char *text  = prefsGetString(m, name, key, NULL);
    +    int32_t     count = 0;
    +
    +    if (text && strcasecmp(text, "all") == 0) {
    +        memcpy(list, src->sizes, sizeof(src->sizes));
    +        count = src->sizeCount;
    +    } else if (text) {
    +        count = parseSizes(name, text, list, DFN_STRIKE_MAX);
    +    }
    +
    +    if (count > 0 && src->sizeCount == 0) {
    +        die("%s: %s is a bitmap face: it has no %s sizes, its glyphs are its own", name, src->path, what);
    +    }
    +
    +    for (int32_t i = 0; i < count; i++) {
    +        if (!hasSize(src->sizes, src->sizeCount, list[i])) {
    +            die("%s: %s size %" PRId32 " is not among its sizes", name, what, list[i]);
    +        }
    +    }
    +
    +    return count;
    +}
    +
    +
    +// Source n of font name ("source", "source1" ...); false when absent
    +static bool readSource(PrefsHandleT *m, const char *name, int32_t n, SourceT *out) {
    +    char        suffix[KEY_MAX] = "";
    +    char        key[KEY_MAX];
    +    const char *path;
    +
    +    if (n > 0) {
    +        snprintf(suffix, sizeof(suffix), "%" PRId32, n);
    +    }
    +
    +    snprintf(key, sizeof(key), "source%s", suffix);
    +    path = prefsGetString(m, name, key, NULL);
    +
    +    if (!path) {
    +        return false;
    +    }
    +
    +    memset(out, 0, sizeof(*out));
    +    snprintf(out->path, sizeof(out->path), "%s", path);
    +    snprintf(key, sizeof(key), "sizes%s", suffix);
    +    out->sizeCount = parseSizes(name, prefsGetString(m, name, key, "native"), out->sizes, DFN_STRIKE_MAX);
    +    snprintf(key, sizeof(key), "hinting%s", suffix);
    +    out->hinting = getChoice(m, name, key, sHinting, DVX_ARRAY_LEN(sHinting), "native");
    +    snprintf(key, sizeof(key), "encoding%s", suffix);
    +    out->cp437 = getChoice(m, name, key, sEncodings, DVX_ARRAY_LEN(sEncodings), "unicode");
    +    snprintf(key, sizeof(key), "weight%s", suffix);
    +    out->weight = prefsGetInt(m, name, key, 0);
    +    snprintf(key, sizeof(key), "smooth%s", suffix);
    +    out->smoothCount = readSizeList(m, name, key, "smoothed", out, out->smooth);
    +    snprintf(key, sizeof(key), "graycut%s", suffix);
    +    out->grayCutCount = readSizeList(m, name, key, "gray-cut", out, out->grayCut);
    +    return true;
    +}
    +
    +
    +// Every strike source src makes, added to strikes.  Returns the source's
    +// copyright notice (faceNotice).
    +static char *renderSource(const char *name, const char *srcDir, const SourceT *src, StrikeT **strikes) {
    +    char    path[DVX_MAX_PATH];
    +    FT_Face face = NULL;
    +    StrikeT s;
    +
    +    snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", srcDir, src->path);
    +
    +    if (FT_New_Face(sLib, path, 0, &face) != 0) {
    +        die("%s: cannot open %s", name, path);
    +    }
    +
    +    if (src->cp437) {
    +        if (face->num_charmaps < 1 || FT_Set_Charmap(face, face->charmaps[0]) != 0) {
    +            die("%s: %s has no character map", name, path);
    +        }
    +    } else if (FT_Select_Charmap(face, FT_ENCODING_UNICODE) != 0) {
    +        die("%s: %s has no Unicode character map", name, path);
    +    }
    +
    +    if (src->weight > 0) {
    +        setWeight(name, path, face, src->weight);
    +    }
    +
    +    if (src->sizeCount == 0) {
    +        if (FT_IS_SCALABLE(face) || face->num_fixed_sizes < 1 || FT_Select_Size(face, 0) != 0) {
    +            die("%s: %s is not a bitmap face (\"native\" sizes)", name, path);
    +        }
    +
    +        renderStrike(name, face, src, (int32_t)((face->available_sizes[0].y_ppem + FT_HALF) >> FT_SHIFT), false, &s);
    +        arrput(*strikes, s);
    +    }
    +
    +    if (src->sizeCount > 0 && !FT_IS_SCALABLE(face)) {
    +        die("%s: %s is a bitmap face (give sizes = native)", name, path);
    +    }
    +
    +    for (int32_t i = 0; i < src->sizeCount; i++) {
    +        if (FT_Set_Pixel_Sizes(face, 0, (FT_UInt)src->sizes[i]) != 0) {
    +            die("%s: %s cannot be set to %" PRId32 " px", name, path, src->sizes[i]);
    +        }
    +
    +        renderStrike(name, face, src, src->sizes[i], false, &s);
    +        arrput(*strikes, s);
    +
    +        if (hasSize(src->smooth, src->smoothCount, src->sizes[i])) {
    +            renderStrike(name, face, src, src->sizes[i], true, &s);
    +            arrput(*strikes, s);
    +        }
    +    }
    +
    +    char *notice = faceNotice(path, face);
    +
    +    FT_Done_Face(face);
    +    return notice;
    +}
    +
    +
    +// The 256 characters of face at its current size into out
    +static void renderStrike(const char *name, FT_Face face, const SourceT *src, int32_t pixelSize, bool gray, StrikeT *out) {
    +    bool     fromGray = gray || hasSize(src->grayCut, src->grayCutCount, pixelSize); // rendered anti-aliased (packBitmap cuts a 1-bit size)
    +    FT_Int32 flags    = FT_LOAD_RENDER | (fromGray ? FT_LOAD_TARGET_LIGHT : FT_LOAD_TARGET_MONO);
    +    uint8_t *packed   = NULL;    // stb_ds
    +    int32_t  ascent   = (int32_t)((face->size->metrics.ascender + FT_HALF) >> FT_SHIFT);
    +    int32_t  descent  = (int32_t)((-face->size->metrics.descender + FT_HALF) >> FT_SHIFT);
    +    int32_t  line     = (int32_t)((face->size->metrics.height + FT_HALF) >> FT_SHIFT);
    +    int32_t  widest   = 0;
    +
    +    if (src->hinting == HintAutoE) {
    +        flags |= FT_LOAD_FORCE_AUTOHINT;
    +    } else if (src->hinting == HintNoneE) {
    +        flags |= FT_LOAD_NO_HINTING;
    +    }
    +
    +    memset(out, 0, sizeof(*out));
    +
    +    for (int32_t c = 0; c < DFN_CHAR_COUNT; c++) {
    +        FontGlyphT *g    = &out->glyphs[c];
    +        FT_ULong    code = src->cp437 ? (FT_ULong)c : dfnCp437[c];
    +        FT_UInt     gi   = (code && src->keep[c]) ? FT_Get_Char_Index(face, code) : 0;
    +
    +        if (gi == 0 || FT_Load_Glyph(face, gi, flags) != 0) {
    +            g->flags = FONT_GLYPH_MISSING;
    +            continue;
    +        }
    +
    +        FT_GlyphSlot     slot = face->glyph;
    +        const FT_Bitmap *bm   = &slot->bitmap;
    +
    +        if (bm->rows > 0 && bm->width > 0 && bm->pixel_mode != (fromGray ? FT_PIXEL_MODE_GRAY : FT_PIXEL_MODE_MONO)) {
    +            die("%s: %s gives a bitmap that is not %s", name, src->path, fromGray ? "8-bit gray" : "1 bit per pixel");
    +        }
    +
    +        bool ink = packBitmap(bm, gray, &packed);
    +
    +        // A face that draws a visible character as nothing lacks it
    +        if (!ink && dfnCp437[c] != UNI_SPACE && dfnCp437[c] != UNI_NBSP) {
    +            g->flags = FONT_GLYPH_MISSING;
    +            continue;
    +        }
    +
    +        int32_t advance = (int32_t)((slot->advance.x + FT_HALF) >> FT_SHIFT);
    +
    +        if (advance < 0 || advance > UINT8_MAX || bm->width > UINT8_MAX || bm->rows > UINT8_MAX || slot->bitmap_left < INT8_MIN || slot->bitmap_left > INT8_MAX || slot->bitmap_top < INT8_MIN || slot->bitmap_top > INT8_MAX) {
    +            die("%s: %s at %" PRId32 " px has a glyph too large for a FontGlyphT (character %02" PRIX32 ")", name, src->path, pixelSize, (uint32_t)c);
    +        }
    +
    +        g->advance = (uint8_t)advance;
    +        g->width   = (uint8_t)bm->width;
    +        g->height  = (uint8_t)bm->rows;
    +        g->left    = (int8_t)slot->bitmap_left;
    +        g->top     = (int8_t)slot->bitmap_top;
    +
    +        if (arrlen(packed) > 0) {
    +            memcpy(arraddnptr(out->bitmaps, arrlen(packed)), packed, (size_t)arrlen(packed));
    +        }
    +
    +        // The tallest glyphs set the line where they pass the face's
    +        // metrics (script faces draw outside their em)
    +        if (g->height > 0) {
    +            ascent  = DVX_MAX(ascent, g->top);
    +            descent = DVX_MAX(descent, g->height - g->top);
    +        }
    +
    +        widest = DVX_MAX(widest, g->advance);
    +    }
    +
    +    if (out->glyphs[CHAR_SPACE].flags & FONT_GLYPH_MISSING) {
    +        die("%s: %s has no space at %" PRId32 " px", name, src->path, pixelSize);
    +    }
    +
    +    arrfree(packed);
    +    out->info.flags       = gray ? DFN_STRIKE_GRAY2 : 0;
    +    out->info.pixelSize   = (uint16_t)pixelSize;
    +    out->info.ascent      = (int16_t)ascent;
    +    out->info.descent     = (int16_t)descent;
    +    out->info.lineHeight  = (int16_t)DVX_MAX(line, ascent + descent);
    +    out->info.maxAdvance  = (uint16_t)widest;
    +    out->info.bitmapBytes = (uint32_t)arrlen(out->bitmaps);
    +}
    +
    +
    +// A variable face at design weight weight, its other axes at their defaults
    +static void setWeight(const char *name, const char *path, FT_Face face, int32_t weight) {
    +    FT_MM_Var *mm    = NULL;
    +    bool       found = false;
    +
    +    if (FT_Get_MM_Var(face, &mm) != 0) {
    +        die("%s: %s is not a variable face (weight given)", name, path);
    +    }
    +
    +    FT_Fixed *coords = (FT_Fixed *)malloc(sizeof(FT_Fixed) * mm->num_axis);
    +
    +    if (!coords) {
    +        die("out of memory");
    +    }
    +
    +    for (FT_UInt a = 0; a < mm->num_axis; a++) {
    +        coords[a] = mm->axis[a].def;
    +
    +        if (mm->axis[a].tag == AXIS_WEIGHT) {
    +            coords[a] = (FT_Fixed)weight << FIXED_SHIFT;
    +            found     = true;
    +        }
    +    }
    +
    +    if (!found || FT_Set_Var_Design_Coordinates(face, mm->num_axis, coords) != 0) {
    +        die("%s: %s cannot be set to weight %" PRId32, name, path, weight);
    +    }
    +
    +    free(coords);
    +    FT_Done_MM_Var(sLib, mm);
    +}
    +
    +
    +// By size, a size's 1-bit strike before its gray one
    +static int strikeCompare(const void *a, const void *b) {
    +    const DfnStrikeT *sa = &((const StrikeT *)a)->info;
    +    const DfnStrikeT *sb = &((const StrikeT *)b)->info;
    +
    +    if (sa->pixelSize != sb->pixelSize) {
    +        return (int32_t)sa->pixelSize - (int32_t)sb->pixelSize;
    +    }
    +
    +    return (int32_t)sa->flags - (int32_t)sb->flags;
    +}
    +
    +
    +// The ASCII form of a non-ASCII character a licence holds; NULL when
    +// there is none
    +static const char *translit(uint32_t cp) {
    +    for (int32_t i = 0; i < DVX_ARRAY_LEN(sTranslit); i++) {
    +        if (sTranslit[i].codepoint == cp) {
    +            return sTranslit[i].ascii;
    +        }
    +    }
    +
    +    return NULL;
    +}
    +
    +
    +// The font: the header (h, its offsets filled in here), the strike
    +// table, each strike's glyphs and bitmaps, then the notice and licence
    +// texts
    +static void writeFont(const char *path, DfnHeaderT *h, StrikeT *strikes, const char *notice, const char *licence) {
    +    int32_t  count  = h->strikeCount;
    +    uint32_t offset = (uint32_t)(sizeof(DfnHeaderT) + sizeof(DfnStrikeT) * (size_t)count);
    +
    +    for (int32_t i = 0; i < count; i++) {
    +        strikes[i].info.glyphOffset   = offset;
    +        offset                       += (uint32_t)sizeof(strikes[i].glyphs);
    +        strikes[i].info.bitmapOffset  = offset;
    +        offset                       += strikes[i].info.bitmapBytes;
    +    }
    +
    +    h->noticeOffset  = offset;
    +    h->noticeBytes   = (uint32_t)strlen(notice);
    +    h->licenceOffset = h->noticeOffset + h->noticeBytes;
    +    h->licenceBytes  = (uint32_t)strlen(licence);
    +
    +    FILE *fp = fopen(path, "wb");
    +
    +    if (!fp) {
    +        die("cannot write %s", path);
    +    }
    +
    +    bool ok = fwrite(h, sizeof(*h), 1, fp) == 1;
    +
    +    for (int32_t i = 0; i < count && ok; i++) {
    +        ok = fwrite(&strikes[i].info, sizeof(strikes[i].info), 1, fp) == 1;
    +    }
    +
    +    for (int32_t i = 0; i < count && ok; i++) {
    +        size_t bytes = strikes[i].info.bitmapBytes;
    +
    +        ok = fwrite(strikes[i].glyphs, sizeof(strikes[i].glyphs), 1, fp) == 1 && fwrite(strikes[i].bitmaps, 1, bytes, fp) == bytes;
    +    }
    +
    +    ok = ok && fwrite(notice, 1, h->noticeBytes, fp) == h->noticeBytes && fwrite(licence, 1, h->licenceBytes, fp) == h->licenceBytes;
    +
    +    if (fclose(fp) != 0 || !ok) {
    +        die("cannot write %s", path);
    +    }
    +}
    +
    +
    +int main(int argc, char **argv) {
    +    if (argc == 4 && strcmp(argv[1], "-p") == 0) {
    +        return preview(argv[2], argv[3]);
    +    }
    +
    +    if (argc == 4 && strcmp(argv[1], "-l") == 0) {
    +        return listSet(argv[2], argv[3]);
    +    }
    +
    +    if (argc != 4) {
    +        fprintf(stderr, "usage: mkfont MANIFEST SOURCEDIR OUTDIR\n       mkfont -l MANIFEST SET\n       mkfont -p FONT.DFN SHEET.BMP\n");
    +        return 2;
    +    }
    +
    +    PrefsHandleT *m = prefsLoad(argv[1]);
    +
    +    if (!m || !prefsKeyName(m, SECTION_FONTS, 0)) {
    +        die("%s has no [%s] list", argv[1], SECTION_FONTS);
    +    }
    +
    +    if (FT_Init_FreeType(&sLib) != 0) {
    +        die("FreeType does not start");
    +    }
    +
    +    if (platformMkdirRecursive(argv[3]) != 0) {
    +        die("cannot make %s", argv[3]);
    +    }
    +
    +    loadLicences(m, argv[2]);
    +
    +    for (int32_t i = 0; prefsKeyName(m, SECTION_FONTS, i); i++) {
    +        const char *name = prefsKeyName(m, SECTION_FONTS, i);
    +
    +        check83("font", name);
    +        buildFont(m, name, argv[2], argv[3]);
    +    }
    +
    +    for (int32_t i = 0; i < arrlen(sLicences); i++) {
    +        arrfree(sLicences[i].text);
    +    }
    +
    +    arrfree(sLicences);
    +    FT_Done_FreeType(sLib);
    +    prefsClose(m);
    +    return 0;
    +}
    diff --git a/src/tools/mkwgticon.c b/src/tools/mkwgticon.c
    index c16f45e4..2762f6f2 100644
    --- a/src/tools/mkwgticon.c
    +++ b/src/tools/mkwgticon.c
    @@ -62,6 +62,8 @@ static void drawListbox(void);
     static void drawListview(void);
     static void drawProgress(void);
     static void drawRadio(void);
    +static void drawRichedit(void);
    +static void drawRichlabel(void);
     static void drawScrollpane(void);
     static void drawSeparator(void);
     static void drawSlider(void);
    @@ -315,6 +317,35 @@ static void drawRadio(void) {
     }
     
     
    +static void drawRichedit(void) {
    +    bmpClearBg(&canvas);
    +    // A text box with a scroll bar: a bold red word, a blue underlined
    +    // one, a line of text and the caret
    +    bmpFillRect(&canvas, 1, 3, 22, 18, 255, 255, 255);
    +    bmpRect(&canvas, 1, 3, 22, 18, 128, 128, 128);
    +    bmpFillRect(&canvas, 19, 4, 3, 16, 192, 192, 192);
    +    miniText(3, 5, "A", 160, 0, 0);
    +    miniText(4, 5, "A", 160, 0, 0);
    +    miniText(9, 5, "B", 0, 0, 160);
    +    bmpHLine(&canvas, 9, 11, 4, 0, 0, 160);
    +    bmpHLine(&canvas, 3, 14, 13, 0, 0, 0);
    +    bmpHLine(&canvas, 3, 17, 8, 0, 0, 0);
    +    bmpVLine(&canvas, 12, 15, 4, 0, 0, 0);
    +}
    +
    +
    +static void drawRichlabel(void) {
    +    bmpClearBg(&canvas);
    +    // A red underlined word and a blue one, then a paragraph's lines
    +    miniText(2, 3, "AB", 160, 0, 0);
    +    bmpHLine(&canvas, 2, 9, 7, 160, 0, 0);
    +    miniText(12, 3, "CD", 0, 0, 160);
    +    bmpHLine(&canvas, 2, 13, 20, 0, 0, 0);
    +    bmpHLine(&canvas, 4, 16, 18, 0, 0, 0);
    +    bmpHLine(&canvas, 2, 19, 14, 0, 0, 0);
    +}
    +
    +
     static void drawScrollpane(void) {
         bmpClearBg(&canvas);
         bmpFillRect(&canvas, 2, 2, 18, 18, 255, 255, 255);
    @@ -581,6 +612,8 @@ int main(int argc, char **argv) {
         static const BmpIconT icons[] = {
             {"button",     drawButton},
             {"label",      drawLabel},
    +        {"richedit",   drawRichedit},
    +        {"richlabel",  drawRichlabel},
             {"textbox",    drawTextbox},
             {"checkbox",   drawCheckbox},
             {"radio",      drawRadio},
    diff --git a/src/tools/proxy/README.md b/src/tools/proxy/README.md
    index 11507d72..3caa3c39 100644
    --- a/src/tools/proxy/README.md
    +++ b/src/tools/proxy/README.md
    @@ -158,7 +158,7 @@ The shim maps COM port indices (0-3) to socket file descriptors via
     layer. Up to 4 ports are supported.
     
     The shim includes the real `rs232.h`, so its constants and signatures
    -cannot drift from the DOS driver, and the packet, security and secLink
    +cannot drift from the DOS driver, and the packet, security, and secLink
     sources compile unmodified. The security library reads `/dev/urandom`
     instead of the DOS timers on Linux.
     
    diff --git a/src/widgets/kpunch/Makefile b/src/widgets/kpunch/Makefile
    index ff561ffc..c3a4d11f 100644
    --- a/src/widgets/kpunch/Makefile
    +++ b/src/widgets/kpunch/Makefile
    @@ -50,6 +50,8 @@ WIDGETS = \
     	listview:listView:widgetListView:listview \
     	progress:progressBar:widgetProgressBar:progress \
     	radio:radio:widgetRadio:radio \
    +	richedit:richEdit:widgetRichEdit:richedit \
    +	richlbl:richLabel:widgetRichLabel:richlbl \
     	scrlpane:scrollPane:widgetScrollPane:scrlpane \
     	separatr:separator:widgetSeparator:separatr \
     	slider:slider:widgetSlider:slider \
    diff --git a/src/widgets/kpunch/ansiTerm/ansiTerm.dhs b/src/widgets/kpunch/ansiTerm/ansiTerm.dhs
    index 3d188adf..48c83b47 100644
    --- a/src/widgets/kpunch/ansiTerm/ansiTerm.dhs
    +++ b/src/widgets/kpunch/ansiTerm/ansiTerm.dhs
    @@ -31,7 +31,9 @@
     
     .h2 AnsiTerm
     
    -A VT100/ANSI-compatible terminal emulator widget designed for connecting to BBS systems over the serial link. Uses a traditional text-mode cell buffer (character + attribute byte pairs) with the CP437 character set and 16-color CGA palette. Supports cursor movement, screen/line erase, scrolling regions, SGR colors, DEC private modes, and a configurable scrollback buffer. Communication is abstracted through read/write function pointers, allowing the terminal to work with raw serial ports, the secLink encrypted channel, or any other byte-oriented transport.
    +A VT100/ANSI-compatible terminal emulator widget designed for connecting to BBS systems over the serial link. Uses a traditional text-mode cell buffer (character + attribute byte pairs) with the CP437 character set and 16-color CGA palette. Supports cursor movement, screen/line erase, scrolling regions, SGR colors, DEC private modes, ANSI music, and a configurable scrollback buffer. Communication is abstracted through read/write function pointers, allowing the terminal to work with raw serial ports, the secLink encrypted channel, or any other byte-oriented transport.
    +
    +WCLASS_FIXED_FONT: it edits by columns, so its font must be fixed-pitch. Any size works (the column math uses the font's charWidth and charHeight); a proportional font set with wgtSetFont, or inherited, is replaced by the system font when it draws.
     
     The widget renders through two paint paths: a full paint used by the normal widget pipeline, and a fast incremental repaint (wgtAnsiTermRepaint) that pushes dirty rows directly to the window content buffer for low-latency serial echo.
     
    @@ -85,6 +87,7 @@ The sApi struct exposes these function pointers:
     
     .index wgtAnsiTermCopy
     .index wgtAnsiTermSetPalette
    +.index wgtAnsiTermSetMusic
     .index AnsiTermExApiT
     
     Features added after the original table live in a second table so AnsiTermApiT keeps its layout for modules already built against it. dvxAnsiTermExApi() returns it (NULL when the loaded widget is older).
    @@ -95,6 +98,7 @@ Features added after the original table live in a second table so AnsiTermApiT k
       header           WGT_API_HEADER                             ABI version and size, checked by wgtGetApi.
       copySelection    bool wgtAnsiTermCopy(w)                    Copy the selection to the clipboard and clear it (a menu's Copy). Returns false when nothing is selected.
       setPalette       void wgtAnsiTermSetPalette(w, rgb)         Replace the 16 attribute colors with ANSITERM_PALETTE_BYTES bytes of red, green, blue triplets (a color scheme). NULL restores the standard CGA colors.
    +  setMusic         void wgtAnsiTermSetMusic(w, on)            Recognize ANSI music (on at first). Off, ESC[M is always Delete Line and ESC[N and ESC[| do nothing.
     .endtable
     
     .h3 Properties (BASIC Interface)
    @@ -102,7 +106,8 @@ Features added after the original table live in a second table so AnsiTermApiT k
     .table
       Property     Type       Access       Description
       --------     ----       ------       -----------
    -  Cols         Integer    Read/Write   Number of columns. Setting it (or Rows) resizes the terminal (1-512): screen and scrollback start empty and the modes the host set (wrap, origin, cursor visible, scroll region) are reset; palette, scrollback length and comm callbacks stay; the window is laid out again.
    +  Cols         Integer    Read/Write   Number of columns. Setting it (or Rows) resizes the terminal (1-512): screen and scrollback start empty and the modes the host set (wrap, origin, cursor visible, scroll region) are reset; palette, scrollback length, and comm callbacks stay; the window is laid out again.
    +  Music        Boolean    Read/Write   Recognize ANSI music (setMusic; True at first).
       Rows         Integer    Read/Write   Number of rows.
       Scrollback   Integer    Read/Write   Maximum number of scrollback lines. Setting it discards the existing history.
     .endtable
    @@ -123,7 +128,20 @@ Features added after the original table live in a second table so AnsiTermApiT k
     
     .h3 Events
     
    -AnsiTerm uses the common events only. No widget-specific events are defined.
    +.index ANSI Music
    +.index ANSITERM_EVT_MUSIC
    +.index ANSITERM_EVT_BELL
    +
    +Besides the common events, two of its own, fired through wgtFireEvent (a C host gets them in onEvent with the names ANSITERM_EVT_MUSIC and ANSITERM_EVT_BELL):
    +
    +.table
    +  Event   Parameters          Description
    +  -----   ----------          -----------
    +  Music   Macro As String     An ANSI music string arrived: the BASIC PLAY text between ESC[| or ESC[N, or ESC[MF (MB, MN, ML, MS), and Ctrl+N (0x0E). For the ESC[M form the macro begins with that M command (e.g. "MFT120O4L8CDE"). Line ends are left out; a string longer than 4096 characters, an empty one, or one cut short by ESC fires nothing.
    +  Bell    (none)              A BEL character (Ctrl+G) arrived.
    +.endtable
    +
    +The widget plays nothing itself: a handler plays the macro (BASIC: PLAY Macro; C: the music library's musicMmlPlay). While a Music or Bell handler runs, the terminal takes no input: Poll leaves the bytes in the connection, and Write keeps them, in order, until the handler returns. A tune played in the foreground (MF) therefore holds back the text after it, as on a DOS terminal, while the window stays responsive. Bare ESC[M (no parameter) followed by anything but F, B, N, L, or S is still Delete Line. A handler may close the terminal's window; the input after the event is then dropped.
     
     .h3 Keyboard Shortcuts
     
    @@ -141,4 +159,4 @@ Within the terminal widget:
       Shift+PgDn       Page forward through the scrollback history.
     .endtable
     
    -Any key sent to the host returns the view to the live screen. The mouse wheel scrolls through the scrollback history locally; it does not send keys to the host. Clearing the screen (ESC[2J, form feed, reset or Clear) saves the screen into the scrollback only up to its last non-blank row.
    +Any key sent to the host returns the view to the live screen. The mouse wheel scrolls through the scrollback history locally; it does not send keys to the host. Clearing the screen (ESC[2J, form feed, reset, or Clear) saves the screen into the scrollback only up to its last non-blank row.
    diff --git a/src/widgets/kpunch/ansiTerm/ansiTerm.h b/src/widgets/kpunch/ansiTerm/ansiTerm.h
    index 71716df8..901fd0c7 100644
    --- a/src/widgets/kpunch/ansiTerm/ansiTerm.h
    +++ b/src/widgets/kpunch/ansiTerm/ansiTerm.h
    @@ -48,8 +48,18 @@ typedef struct {
         WgtApiHeaderT header;
         bool (*copySelection)(WidgetT *w);                   // selection to the clipboard; false when nothing is selected
         void (*setPalette)(WidgetT *w, const uint8_t *rgb);  // ANSITERM_PALETTE_BYTES bytes; NULL restores the standard colors
    +    void (*setMusic)(WidgetT *w, bool on);               // recognise ANSI music (on at first)
     } AnsiTermExApiT;
     
    +// Events (the widget's onEvent; BASIC Terminal_Bell and Terminal_Music).
    +// Music hands over an ANSI music string -- the BASIC PLAY text between
    +// ESC[| or ESC[N or ESC[MF (MB, MN, ML, MS) and Ctrl+N -- as its one
    +// string argument; Bell is a BEL (Ctrl+G).  The terminal takes no input
    +// until the handler returns, so a tune played in the foreground holds
    +// back the text that follows it, as on a DOS terminal.
    +#define ANSITERM_EVT_BELL  "Bell"
    +#define ANSITERM_EVT_MUSIC "Music"
    +
     // Consumer side: the accessor and convenience macros.  The widget's own
     // implementation defines ANSITERM_IMPL so its functions keep their names.
     #ifndef ANSITERM_IMPL
    @@ -78,6 +88,7 @@ static inline const AnsiTermExApiT *dvxAnsiTermExApi(void) {
     
     #define wgtAnsiTermCopy(w)                                   dvxAnsiTermExApi()->copySelection(w)
     #define wgtAnsiTermSetPalette(w, rgb)                        dvxAnsiTermExApi()->setPalette(w, rgb)
    +#define wgtAnsiTermSetMusic(w, on)                           dvxAnsiTermExApi()->setMusic(w, on)
     
     #endif // ANSITERM_IMPL
     
    diff --git a/src/widgets/kpunch/ansiTerm/ansiterm.bhs b/src/widgets/kpunch/ansiTerm/ansiterm.bhs
    index 059aa755..6bd4223b 100644
    --- a/src/widgets/kpunch/ansiTerm/ansiterm.bhs
    +++ b/src/widgets/kpunch/ansiTerm/ansiterm.bhs
    @@ -34,6 +34,8 @@ DVX Extension -- DVX Widget: ansiterm
     
     A VT100/ANSI terminal emulator with a text-mode cell buffer, 16-color CGA palette, and scrollback history. Processes standard ANSI escape sequences (cursor control, erase, scroll, SGR colors, DEC private modes) typically used by DOS BBS software. Default size is 80 columns by 25 rows. Supports text selection with the mouse, Ctrl+C to copy, and Ctrl+V to send clipboard text as input. Shift+PgUp and Shift+PgDn page through the scrollback history and the mouse wheel scrolls it; any other key is sent to the host and returns the view to the live screen.
     
    +Its font must be fixed-pitch, every character the same width; given a proportional font it draws in the system font instead. A fixed-pitch font of any size changes the size of every cell.
    +
     .h2 Type-Specific Properties
     
     .table
    @@ -62,7 +64,7 @@ No default event. Uses common events only.
     
     .h2 Serial Communication
     
    -The conn.bas library connects a Terminal the way the Terminal program does -- a serial port, a modem, the secure link or Telnet -- with capture, answerback and file transfers (ConnAttach; see Include Libraries).  For a packet link of your own, use the comm.bas library:
    +The conn.bas library connects a Terminal the way the Terminal program does -- a serial port, a modem, the secure link, or Telnet -- with capture, answerback, and file transfers (ConnAttach; see Include Libraries).  For a packet link of your own, use the comm.bas library:
     
     .code
     Dim link As Long
    diff --git a/src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c b/src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c
    index 85130865..ad3952e1 100644
    --- a/src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c
    +++ b/src/widgets/kpunch/ansiTerm/widgetAnsiTerm.c
    @@ -35,9 +35,19 @@
     //   3. Dirty-row tracking via a per-row bitmask allows sub-millisecond
     //      incremental repaints without scanning the entire buffer
     //
    -// The ANSI parser is a 3-state machine (NORMAL -> ESC -> CSI) that handles
    -// the subset of sequences commonly used by DOS BBS software: cursor movement,
    -// screen/line erase, scrolling regions, SGR colors, and a few DEC private modes.
    +// The ANSI parser is a state machine (NORMAL -> ESC -> CSI, and the ANSI
    +// music states) that handles the subset of sequences commonly used by DOS
    +// BBS software: cursor movement, screen/line erase, scrolling regions, SGR
    +// colors, a few DEC private modes, and ANSI music.
    +//
    +// ANSI music is BASIC PLAY text between ESC[ and Ctrl+N (0x0E): after
    +// ESC[| or ESC[N always, and after ESC[M when the next character makes it
    +// MF, MB, MN, ML or MS (bare ESC[M is otherwise Delete Line, which it
    +// shares).  The finished string goes to the Music event (Macro As String),
    +// a BEL to the Bell event; the widget plays nothing itself.  While such an
    +// event runs the terminal takes no input: Poll leaves it in the
    +// connection and Write keeps it, in order, until the handler returns, so
    +// a tune played in the foreground (MF) holds back the text after it.
     // Full VT100 conformance is explicitly NOT a goal  -- only sequences actually
     // emitted by real BBS systems are implemented.
     //
    @@ -65,6 +75,9 @@
     #include "dvxWgtP.h"
     #include "ansiTerm.h"
     #include "dvxPlat.h"
    +#include "stb_ds_wrap.h"
    +
    +#include 
     #include "../../../libs/kpunch/texthelp/textHelp.h"
     
     // ABI stamp checked by the loader (see DVX_ABI_VERSION).
    @@ -81,9 +94,17 @@ static int32_t sTypeId = -1;
     #define ANSI_DEFAULT_SCROLLBACK 500
     #define ANSI_DEFAULT_COLS       80
     #define ANSI_DEFAULT_ROWS       25
    -#define PARSE_NORMAL  0
    -#define PARSE_ESC     1
    -#define PARSE_CSI     2
    +#define PARSE_NORMAL      0
    +#define PARSE_ESC         1
    +#define PARSE_CSI         2
    +#define PARSE_MUSIC_PEEK  3     // after ESC[M: the next character says music or Delete Line
    +#define PARSE_MUSIC       4     // collecting a music string
    +#define ANSI_MUSIC_END    0x0E  // Ctrl+N ends a music string
    +#define ANSI_MUSIC_MAX    4096  // a longer string is dropped
    +#define ANSI_MUSIC_MODES  "FBNLS"   // ESC[M then one of these: MF, MB, MN, ML, MS
    +#define ANSI_READY_NONE   0     // what the last byte parsed finished for an event
    +#define ANSI_READY_MUSIC  1
    +#define ANSI_READY_BELL   2
     #define ANSI_DEFAULT_ATTR  0x07
     #define BLINK_MS           500
     #define ANSI_TAB_WIDTH     8
    @@ -129,6 +150,10 @@ typedef struct {
         int32_t  scrollTop;
         int32_t  scrollBot;
         uint8_t *scrollback;
    +    char    *music;         // stb_ds: the music string being collected
    +    uint8_t *pending;       // stb_ds: input waiting while an event runs (or left after one)
    +    bool    *alive;         // while an event runs: cleared when the terminal is destroyed
    +    int32_t  pendingAt;     // the next byte of pending to parse
         int32_t  scrollbackMax;
         int32_t  scrollbackCount;
         int32_t  scrollbackHead;
    @@ -138,6 +163,8 @@ typedef struct {
         uint32_t *dirtyRows;    // ANSI_DIRTY_WORDS(rows) words, one bit per row
         int32_t  lastCursorRow;
         int32_t  lastCursorCol;
    +    int32_t  movedRows;     // whole-screen scrolls since the last paint: the quick repaint moves the drawn rows up this far
    +
         uint32_t packedPalette[ANSI_PALETTE_SIZE];
         uint8_t  palette[ANSI_PALETTE_SIZE][3];     // RGB of each attribute color
         int32_t  selStartLine;
    @@ -160,6 +187,10 @@ typedef struct {
         bool     hasBlinkCells;     // tracked to skip the per-blink O(rows*cols) scan
         bool     sbDirty;           // scrollbar needs redrawing by the quick repaint
         bool     sbDragging;        // current drag is on the scrollbar thumb
    +    bool     musicOn;           // ANSI music is recognised (Music property)
    +    bool     musicOverflow;     // the string being collected outgrew ANSI_MUSIC_MAX
    +    bool     inEvent;           // a Music or Bell event runs: input waits
    +    uint8_t  ready;             // ANSI_READY_*: an event the parse finished
         int32_t  sbDragOff;         // grab offset within the thumb
     } AnsiTermDataT;
     
    @@ -207,14 +238,21 @@ static bool            ansiTermDirtyAny(const WidgetT *w);
     static void            ansiTermDirtyClear(WidgetT *w);
     static void            ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count);
     static void            ansiTermDirtyRow(WidgetT *w, int32_t row);
    +static void            ansiTermDirtyShiftUp(WidgetT *w);
     static bool            ansiTermDirtyTest(const WidgetT *w, int32_t row);
     static void            ansiTermDispatchCsi(WidgetT *w, uint8_t cmd);
    +static bool            ansiTermDrain(WidgetT *w);
     static void            ansiTermEraseDisplay(WidgetT *w, int32_t mode);
     static void            ansiTermEraseLine(WidgetT *w, int32_t mode);
    +static bool            ansiTermFeed(WidgetT *w, const uint8_t *data, int32_t len);
     static void            ansiTermFillCells(WidgetT *w, int32_t start, int32_t count);
    +static bool            ansiTermFireReady(WidgetT *w);
     static const uint8_t  *ansiTermGetLine(WidgetT *w, int32_t lineIndex);
     static bool            ansiTermHasSelection(const WidgetT *w);
     static void            ansiTermInsertLines(WidgetT *w, int32_t count);
    +static void            ansiTermMoveRows(WidgetT *w, DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t width, int32_t cellH);
    +static void            ansiTermMusicByte(WidgetT *w, uint8_t ch);
    +static void            ansiTermMusicStart(WidgetT *w, const char *lead);
     static void            ansiTermNewline(WidgetT *w);
     static void            ansiTermPaintSelRow(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t screenRow, int32_t baseX, int32_t baseY);
     static void            ansiTermPasteToComm(WidgetT *w);
    @@ -237,11 +275,13 @@ static void            wgtAnsiTermBasSetRows(WidgetT *w, int32_t rows);
     void                   wgtAnsiTermClear(WidgetT *w);
     static bool            wgtAnsiTermCopy(WidgetT *w);
     static int32_t         wgtAnsiTermGetCols(const WidgetT *w);
    +static bool            wgtAnsiTermGetMusic(const WidgetT *w);
     static int32_t         wgtAnsiTermGetRows(const WidgetT *w);
     static int32_t         wgtAnsiTermGetScrollback(const WidgetT *w);
     int32_t                wgtAnsiTermPoll(WidgetT *w);
     int32_t                wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH);
     void                   wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t *, int32_t), int32_t (*writeFn)(void *, const uint8_t *, int32_t));
    +static void            wgtAnsiTermSetMusic(WidgetT *w, bool on);
     static void            wgtAnsiTermSetPalette(WidgetT *w, const uint8_t *rgb);
     void                   wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines);
     void                   wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len);
    @@ -360,12 +400,14 @@ static void ansiTermDeleteLines(WidgetT *w, int32_t count) {
     }
     
     
    +// Every row is drawn again: none needs moving
     static void ansiTermDirtyAll(WidgetT *w) {
         AnsiTermDataT *at   = (AnsiTermDataT *)w->data;
         int32_t       words = ANSI_DIRTY_WORDS(at->rows);
         for (int32_t i = 0; i < words; i++) {
             at->dirtyRows[i] = ANSI_DIRTY_WORD_ALL;
         }
    +    at->movedRows = 0;
     }
     
     
    @@ -381,12 +423,14 @@ static bool ansiTermDirtyAny(const WidgetT *w) {
     }
     
     
    +// After a paint (or one dropped while hidden): nothing to draw or move
     static void ansiTermDirtyClear(WidgetT *w) {
         AnsiTermDataT *at   = (AnsiTermDataT *)w->data;
         int32_t       words = ANSI_DIRTY_WORDS(at->rows);
         for (int32_t i = 0; i < words; i++) {
             at->dirtyRows[i] = 0;
         }
    +    at->movedRows = 0;
     }
     
     
    @@ -408,6 +452,25 @@ static void ansiTermDirtyRow(WidgetT *w, int32_t row) {
     }
     
     
    +// The screen's cells moved up one row: each row's dirty bit moves with
    +// them (the top row's drops off; the bottom row's is clear until the new
    +// row is filled)
    +static void ansiTermDirtyShiftUp(WidgetT *w) {
    +    AnsiTermDataT *at = (AnsiTermDataT *)w->data;
    +
    +    for (int32_t row = 0; row < at->rows; row++) {
    +        uint32_t  bit  = 1U << (row % ANSI_DIRTY_WORD_BITS);
    +        uint32_t *word = &at->dirtyRows[row / ANSI_DIRTY_WORD_BITS];
    +
    +        if (row + 1 < at->rows && ansiTermDirtyTest(w, row + 1)) {
    +            *word |= bit;
    +        } else {
    +            *word &= ~bit;
    +        }
    +    }
    +}
    +
    +
     static bool ansiTermDirtyTest(const WidgetT *w, int32_t row) {
         const AnsiTermDataT *at = (const AnsiTermDataT *)w->data;
         return (at->dirtyRows[row / ANSI_DIRTY_WORD_BITS] & (1U << (row % ANSI_DIRTY_WORD_BITS))) != 0;
    @@ -560,7 +623,20 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
                 break;
             }
             case 'M': {
    -            ansiTermDeleteLines(w, (n >= 1 && p[0]) ? p[0] : 1);
    +            // Bare ESC[M may begin ANSI music: the next character decides
    +            if (at->musicOn && n == 0) {
    +                at->parseState = PARSE_MUSIC_PEEK;
    +            } else {
    +                ansiTermDeleteLines(w, (n >= 1 && p[0]) ? p[0] : 1);
    +            }
    +            break;
    +        }
    +        case 'N':
    +        case '|': {
    +            // ANSI music for certain: BANSI's ESC[N, SyncTERM's ESC[|
    +            if (at->musicOn && n == 0) {
    +                ansiTermMusicStart(w, "");
    +            }
                 break;
             }
             case 'P': {
    @@ -663,6 +739,29 @@ static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) {
     }
     
     
    +// Parse the input waiting in pending, unless an event runs; what arrives
    +// while one does goes on the end.  False when an event destroyed the
    +// terminal.
    +static bool ansiTermDrain(WidgetT *w) {
    +    AnsiTermDataT *at = (AnsiTermDataT *)w->data;
    +
    +    while (!at->inEvent && at->pendingAt < arrlen(at->pending)) {
    +        ansiTermProcessByte(w, at->pending[at->pendingAt++]);
    +
    +        if (at->ready != ANSI_READY_NONE && !ansiTermFireReady(w)) {
    +            return false;
    +        }
    +    }
    +
    +    if (!at->inEvent) {
    +        arrfree(at->pending);
    +        at->pendingAt = 0;
    +    }
    +
    +    return true;
    +}
    +
    +
     static void ansiTermEraseDisplay(WidgetT *w, int32_t mode) {
         AnsiTermDataT *at  = (AnsiTermDataT *)w->data;
         int32_t       cols = at->cols;
    @@ -695,6 +794,30 @@ static void ansiTermEraseLine(WidgetT *w, int32_t mode) {
     }
     
     
    +// Parse len bytes while nothing waits in pending.  A byte that finishes
    +// a Music or Bell event stops it there: the rest waits in pending and the
    +// event fires.  False when the event destroyed the terminal.
    +static bool ansiTermFeed(WidgetT *w, const uint8_t *data, int32_t len) {
    +    AnsiTermDataT *at = (AnsiTermDataT *)w->data;
    +
    +    for (int32_t i = 0; i < len; i++) {
    +        ansiTermProcessByte(w, data[i]);
    +
    +        if (at->ready != ANSI_READY_NONE) {
    +            int32_t rest = len - i - 1;
    +
    +            if (rest > 0) {
    +                memcpy(arraddnptr(at->pending, rest), data + i + 1, (size_t)rest);
    +            }
    +
    +            return ansiTermFireReady(w);
    +        }
    +    }
    +
    +    return true;
    +}
    +
    +
     static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count) {
         AnsiTermDataT *at    = (AnsiTermDataT *)w->data;
         uint8_t       *cells = at->cells;
    @@ -715,6 +838,36 @@ static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count) {
     }
     
     
    +// Fire the event the parse finished: Music with its string, or Bell.  The
    +// terminal takes no input until the handler returns.  False when the
    +// handler destroyed the terminal (its window closed).
    +static bool ansiTermFireReady(WidgetT *w) {
    +    AnsiTermDataT *at    = (AnsiTermDataT *)w->data;
    +    uint8_t        ready = at->ready;
    +    bool           alive = true;
    +
    +    at->ready   = ANSI_READY_NONE;
    +    at->inEvent = true;
    +    at->alive   = &alive;
    +
    +    if (ready == ANSI_READY_MUSIC) {
    +        WgtValueT arg = wgtValueStr(at->music);
    +
    +        wgtFireEvent(w, ANSITERM_EVT_MUSIC, &arg, 1);
    +    } else {
    +        wgtFireEvent(w, ANSITERM_EVT_BELL, NULL, 0);
    +    }
    +
    +    if (!alive) {
    +        return false;
    +    }
    +
    +    at->alive   = NULL;
    +    at->inEvent = false;
    +    return true;
    +}
    +
    +
     static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex) {
         AnsiTermDataT *at         = (AnsiTermDataT *)w->data;
         int32_t       cols        = at->cols;
    @@ -768,6 +921,71 @@ static void ansiTermInsertLines(WidgetT *w, int32_t count) {
     }
     
     
    +// Move the text area's drawn rows (at x, y, width wide) up movedRows rows:
    +// the screen scrolled that far since they were drawn.  When d's clip cuts
    +// the text area vertically some would come from pixels never drawn, so
    +// every row is drawn again instead.
    +static void ansiTermMoveRows(WidgetT *w, DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t width, int32_t cellH) {
    +    AnsiTermDataT *at    = (AnsiTermDataT *)w->data;
    +    int32_t        shift = at->movedRows * cellH;
    +    int32_t        h     = at->rows * cellH;
    +    int32_t        x0    = DVX_MAX(x, d->clipX);
    +    int32_t        x1    = DVX_MIN(x + width, d->clipX + d->clipW);
    +    int32_t        bpp   = ops->bytesPerPixel;
    +
    +    if (y < d->clipY || y + h > d->clipY + d->clipH) {
    +        ansiTermDirtyAll(w);
    +        return;
    +    }
    +
    +    for (int32_t row = y; row < y + h - shift && x1 > x0; row++) {
    +        uint8_t *dst = d->backBuf + row * d->pitch + x0 * bpp;
    +
    +        ops->spanCopy(dst, dst + shift * d->pitch, x1 - x0);
    +    }
    +}
    +
    +
    +// A character of a music string: Ctrl+N ends it (and readies the Music
    +// event unless it is empty or outgrew ANSI_MUSIC_MAX), ESC abandons it
    +// for the sequence that starts, and line ends are left out
    +static void ansiTermMusicByte(WidgetT *w, uint8_t ch) {
    +    AnsiTermDataT *at = (AnsiTermDataT *)w->data;
    +
    +    if (ch == ANSI_MUSIC_END) {
    +        at->parseState = PARSE_NORMAL;
    +
    +        if (!at->musicOverflow && arrlen(at->music) > 0) {
    +            arrput(at->music, '\0');
    +            at->ready = ANSI_READY_MUSIC;
    +        }
    +    } else if (ch == KEY_ESCAPE) {
    +        at->parseState = PARSE_ESC;
    +    } else if (ch == '\r' || ch == '\n') {
    +        return;
    +    } else if (arrlen(at->music) >= ANSI_MUSIC_MAX) {
    +        at->musicOverflow = true;
    +    } else {
    +        arrput(at->music, (char)ch);
    +    }
    +}
    +
    +
    +// Begin a music string with lead (the "M" and mode letter of an ESC[M one)
    +static void ansiTermMusicStart(WidgetT *w, const char *lead) {
    +    AnsiTermDataT *at = (AnsiTermDataT *)w->data;
    +
    +    arrsetlen(at->music, 0);
    +
    +    for (; *lead; lead++) {
    +        arrput(at->music, *lead);
    +    }
    +
    +    at->musicOverflow = false;
    +    at->parseState    = PARSE_MUSIC;
    +}
    +
    +
     static void ansiTermNewline(WidgetT *w) {
         AnsiTermDataT *at = (AnsiTermDataT *)w->data;
         if (at->cursorRow == at->scrollBot) {
    @@ -865,7 +1083,7 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) {
                     at->cursorRow = 0;
                     at->cursorCol = 0;
                 } else if (ch == '\a') {
    -                // Bell -- ignored
    +                at->ready = ANSI_READY_BELL;
                 } else {
                     ansiTermPutChar(w, ch);
                 }
    @@ -929,12 +1147,29 @@ static void ansiTermProcessByte(WidgetT *w, uint8_t ch) {
                         at->paramCount++;
                     }
                 } else if (ch >= 0x40 && ch <= 0x7E) {
    -                ansiTermDispatchCsi(w, ch);
    +                // Before the dispatch, which may begin a music string
                     at->parseState = PARSE_NORMAL;
    +                ansiTermDispatchCsi(w, ch);
                 } else {
                     at->parseState = PARSE_NORMAL;
                 }
                 break;
    +        case PARSE_MUSIC_PEEK:
    +            // ESC[M and a mode letter (MF, MB, ...) is music; anything else
    +            // leaves it Delete Line, and the character is parsed as usual
    +            if (ch != '\0' && strchr(ANSI_MUSIC_MODES, toupper(ch))) {
    +                const char lead[] = { 'M', (char)toupper(ch), '\0' };
    +
    +                ansiTermMusicStart(w, lead);
    +            } else {
    +                at->parseState = PARSE_NORMAL;
    +                ansiTermDeleteLines(w, 1);
    +                ansiTermProcessByte(w, ch);
    +            }
    +            break;
    +        case PARSE_MUSIC:
    +            ansiTermMusicByte(w, ch);
    +            break;
         }
     }
     
    @@ -1135,13 +1370,17 @@ static void ansiTermScrollDown(WidgetT *w) {
     }
     
     
    +// A whole screen shown live scrolls by moving its drawn rows (movedRows,
    +// ansiTermMoveRows): only the new bottom row and rows not yet drawn are
    +// drawn.  A scroll region or a view into the scrollback draws its rows.
     static void ansiTermScrollUp(WidgetT *w) {
         AnsiTermDataT *at         = (AnsiTermDataT *)w->data;
         int32_t       cols        = at->cols;
         int32_t       top         = at->scrollTop;
         int32_t       bot         = at->scrollBot;
         int32_t       bytesPerRow = cols * 2;
    -    if (top == 0 && bot == at->rows - 1) {
    +    bool          whole       = (top == 0 && bot == at->rows - 1);
    +    if (whole) {
             bool wasAtBottom = (at->scrollPos == at->scrollbackCount);
             ansiTermAddToScrollback(w, 0);
             if (wasAtBottom) {
    @@ -1151,6 +1390,12 @@ static void ansiTermScrollUp(WidgetT *w) {
         if (bot > top) {
             memmove(at->cells + top * bytesPerRow, at->cells + (top + 1) * bytesPerRow, (bot - top) * bytesPerRow);
         }
    +    if (whole && at->scrollPos == at->scrollbackCount) {
    +        ansiTermDirtyShiftUp(w);
    +        at->movedRows = DVX_MIN(at->movedRows + 1, at->rows);
    +        ansiTermFillCells(w, bot * cols, cols);
    +        return;
    +    }
         ansiTermFillCells(w, bot * cols, cols);
         for (int32_t r = top; r <= bot; r++) {
             ansiTermDirtyRow(w, r);
    @@ -1258,6 +1503,7 @@ WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) {
         at->cursorTime    = platformClock();
         at->lastCursorRow = -1;
         at->lastCursorCol = -1;
    +    at->musicOn       = true;
         ansiTermResetScreen(w);
         return w;
     }
    @@ -1340,6 +1586,13 @@ static int32_t wgtAnsiTermGetCols(const WidgetT *w) {
     }
     
     
    +static bool wgtAnsiTermGetMusic(const WidgetT *w) {
    +    VALIDATE_WIDGET(w, sTypeId, false);
    +    const AnsiTermDataT *at = (const AnsiTermDataT *)w->data;
    +    return at->musicOn;
    +}
    +
    +
     static int32_t wgtAnsiTermGetRows(const WidgetT *w) {
         VALIDATE_WIDGET(w, sTypeId, 0);
         AnsiTermDataT *at = (AnsiTermDataT *)w->data;
    @@ -1388,7 +1641,8 @@ int32_t wgtAnsiTermPoll(WidgetT *w) {
             at->cursorOn   = !at->cursorOn;
             ansiTermDirtyRow(w, at->cursorRow);
         }
    -    if (!at->commRead) {
    +    // While an event runs the input stays in the connection
    +    if (!at->commRead || at->inEvent) {
             return 0;
         }
         uint8_t buf[ANSI_POLL_BUF];
    @@ -1407,16 +1661,8 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) {
     
         AnsiTermDataT *at = (AnsiTermDataT *)w->data;
     
    -    // A hidden terminal (inactive tab page, hidden ancestor) must not draw
    -    // into the content buffer.  Drop the pending rows: the full paint that
    -    // runs when it is shown again redraws every row.
    -    if (!widgetIsShown(w)) {
    -        ansiTermDirtyClear(w);
    -        at->sbDirty = false;
    -        return 0;
    -    }
    -
    -    int32_t prevRow = at->lastCursorRow;
    +    // The old cursor moved up with the rows the screen scrolled
    +    int32_t prevRow = at->lastCursorRow - at->movedRows;
         int32_t curRow  = at->cursorRow;
     
         if (prevRow != curRow || at->lastCursorCol != at->cursorCol) {
    @@ -1428,53 +1674,24 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) {
             return 0;
         }
     
    -    WindowT *win = w->window;
    +    // A hidden or clipped-away terminal draws nothing: the pending rows are
    +    // dropped, the full paint that runs when it shows again redraws them
    +    DisplayT     cd;
    +    AppContextT *ctx = wgtGetContext(w);
     
    -    if (!win->contentBuf || !win->widgetRoot) {
    -        return 0;
    -    }
    -
    -    AppContextT *ctx = wgtGetContext(win->widgetRoot);
    -
    -    if (!ctx) {
    -        return 0;
    -    }
    -
    -    DisplayT cd;
    -    RectT    clip = { 0, 0, win->contentW, win->contentH };
    -    bool     vis  = true;
    -
    -    dvxContentDisplay(ctx, win, &cd);
    -
    -    // Clip to the widget and every ancestor.  Inside a scroll container the
    -    // visible viewport is not the ancestor rect (its scrollbars and border
    -    // sit inside it), so leave those to the normal clipped paint path.
    -    for (const WidgetT *p = w; p; p = p->parent) {
    -        if (p != w && p->wclass && (p->wclass->flags & WCLASS_SCROLL_CONTAINER)) {
    -            wgtInvalidatePaint(w);
    -            return 0;
    -        }
    -
    -        RectT pr = { p->x, p->y, p->w, p->h };
    -
    -        vis = vis && rectIntersect(&clip, &pr, &clip);
    -    }
    -
    -    if (!vis) {
    +    if (!widgetQuickPaintBegin(w, &cd)) {
             ansiTermDirtyClear(w);
             at->sbDirty = false;
             return 0;
         }
     
    -    setClipRect(&cd, clip.x, clip.y, clip.w, clip.h);
    -
    -    const BlitOpsT    *ops  = &ctx->blitOps;
    -    const BitmapFontT *font = &ctx->font;
    -    int32_t           cols  = at->cols;
    -    int32_t           rows  = at->rows;
    -    int32_t           cellH = font->charHeight;
    -    int32_t           baseX = w->x + ANSI_BORDER;
    -    int32_t           baseY = w->y + ANSI_BORDER;
    +    const BlitOpsT    *ops   = &ctx->blitOps;
    +    const BitmapFontT *font  = wgtFont(w);
    +    int32_t            cols  = at->cols;
    +    int32_t            rows  = at->rows;
    +    int32_t            cellH = font->charHeight;
    +    int32_t            baseX = w->x + ANSI_BORDER;
    +    int32_t            baseY = w->y + ANSI_BORDER;
         ansiTermBuildPalette(w, &cd);
         const uint32_t *palette    = at->packedPalette;
         bool           viewingLive = (at->scrollPos == at->scrollbackCount);
    @@ -1482,6 +1699,15 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) {
         int32_t        minRow      = rows;
         int32_t        maxRow      = -1;
     
    +    // Rows the screen scrolled since they were drawn move up as they are;
    +    // the whole text area changes on screen
    +    if (at->movedRows > 0) {
    +        ansiTermMoveRows(w, &cd, ops, baseX, baseY, cols * font->charWidth, cellH);
    +        minRow = 0;
    +        maxRow = rows - 1;
    +        repainted++;
    +    }
    +
         for (int32_t row = 0; row < rows; row++) {
             if (!ansiTermDirtyTest(w, row)) {
                 continue;
    @@ -1546,6 +1772,15 @@ void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t
     
     // Replace the 16 attribute colors (a color scheme); NULL restores the
     // standard CGA colors.  The next paint repacks them for the display.
    +// Recognise ANSI music (on at first).  Off, ESC[M is always Delete Line
    +// and ESC[N and ESC[| do nothing.
    +static void wgtAnsiTermSetMusic(WidgetT *w, bool on) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    AnsiTermDataT *at = (AnsiTermDataT *)w->data;
    +    at->musicOn = on;
    +}
    +
    +
     static void wgtAnsiTermSetPalette(WidgetT *w, const uint8_t *rgb) {
         VALIDATE_WIDGET_VOID(w, sTypeId);
         AnsiTermDataT *at = (AnsiTermDataT *)w->data;
    @@ -1589,10 +1824,17 @@ void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len) {
         if (!w || w->type != sTypeId || !data || len <= 0) {
             return;
         }
    +    AnsiTermDataT *at = (AnsiTermDataT *)w->data;
         ansiTermClearSelection(w);
    -    for (int32_t i = 0; i < len; i++) {
    -        ansiTermProcessByte(w, data[i]);
    +
    +    // Behind any input waiting (while an event runs, it only waits)
    +    if (at->inEvent || arrlen(at->pending) > 0) {
    +        memcpy(arraddnptr(at->pending, len), data, (size_t)len);
    +    } else if (!ansiTermFeed(w, data, len)) {
    +        return;
         }
    +
    +    ansiTermDrain(w);
     }
     
     
    @@ -1631,9 +1873,15 @@ static bool widgetAnsiTermClearSelection(WidgetT *w) {
     void widgetAnsiTermDestroy(WidgetT *w) {
         AnsiTermDataT *at = (AnsiTermDataT *)w->data;
         if (at) {
    +        // Destroyed by an event's handler: the parse stops when it returns
    +        if (at->alive) {
    +            *at->alive = false;
    +        }
             free(at->cells);
             free(at->scrollback);
             free(at->dirtyRows);
    +        arrfree(at->music);
    +        arrfree(at->pending);
             free(at);
             w->data = NULL;
         }
    @@ -1642,11 +1890,10 @@ void widgetAnsiTermDestroy(WidgetT *w) {
     
     static void widgetAnsiTermOnDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
         (void)root;
    -    AnsiTermDataT     *at   = (AnsiTermDataT *)w->data;
    -    AppContextT       *ctx  = wgtGetContext(w);
    -    const BitmapFontT *font = &ctx->font;
    -    int32_t           innerX = w->x + ANSI_BORDER;
    -    int32_t           innerY = w->y + ANSI_BORDER;
    +    AnsiTermDataT     *at     = (AnsiTermDataT *)w->data;
    +    const BitmapFontT *font   = wgtFont(w);
    +    int32_t            innerX = w->x + ANSI_BORDER;
    +    int32_t            innerY = w->y + ANSI_BORDER;
     
         if (at->sbDragging) {
             int32_t trackH   = at->rows * font->charHeight - WGT_SB_W * 2;
    @@ -1729,9 +1976,9 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) {
     
     
     void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
    +    (void)root;
         AnsiTermDataT     *at   = (AnsiTermDataT *)hit->data;
    -    AppContextT       *actx = (AppContextT *)root->userData;
    -    const BitmapFontT *font = &actx->font;
    +    const BitmapFontT *font = wgtFont(hit);
         sFocusedWidget = hit;
         clearOtherSelections(hit);
         int32_t cols = at->cols;
    @@ -1871,7 +2118,7 @@ void widgetAnsiTermPollVtable(WidgetT *w, WindowT *win) {
     
     static const WidgetClassT sClassAnsiTerm = {
         .version = WGT_CLASS_VERSION,
    -    .flags   = WCLASS_FOCUSABLE | WCLASS_NEEDS_POLL | WCLASS_SWALLOWS_TAB,
    +    .flags   = WCLASS_FOCUSABLE | WCLASS_NEEDS_POLL | WCLASS_SWALLOWS_TAB | WCLASS_FIXED_FONT,
         .handlers = {
             [WGT_METHOD_PAINT]           = (void *)widgetAnsiTermPaint,
             [WGT_METHOD_CALC_MIN_SIZE]   = (void *)widgetAnsiTermCalcMinSize,
    @@ -1901,13 +2148,15 @@ static const AnsiTermApiT sApi = {
     static const AnsiTermExApiT sApiEx = {
         .header        = WGT_API_HEADER(AnsiTermExApiT),
         .copySelection = wgtAnsiTermCopy,
    -    .setPalette    = wgtAnsiTermSetPalette
    +    .setPalette    = wgtAnsiTermSetPalette,
    +    .setMusic      = wgtAnsiTermSetMusic
     };
     
     static const WgtPropDescT sProps[] = {
    -    { "Cols",       WGT_IFACE_INT, (void *)wgtAnsiTermGetCols, (void *)wgtAnsiTermBasSetCols, NULL },
    -    { "Rows",       WGT_IFACE_INT, (void *)wgtAnsiTermGetRows, (void *)wgtAnsiTermBasSetRows, NULL },
    -    { "Scrollback", WGT_IFACE_INT, (void *)wgtAnsiTermGetScrollback, (void *)wgtAnsiTermSetScrollback, NULL }
    +    { "Cols",       WGT_IFACE_INT,  (void *)wgtAnsiTermGetCols,  (void *)wgtAnsiTermBasSetCols, NULL },
    +    { "Music",      WGT_IFACE_BOOL, (void *)wgtAnsiTermGetMusic, (void *)wgtAnsiTermSetMusic,   NULL },
    +    { "Rows",       WGT_IFACE_INT,  (void *)wgtAnsiTermGetRows,  (void *)wgtAnsiTermBasSetRows, NULL },
    +    { "Scrollback", WGT_IFACE_INT,  (void *)wgtAnsiTermGetScrollback, (void *)wgtAnsiTermSetScrollback, NULL }
     };
     
     static const WgtMethodDescT sMethods[] = {
    @@ -1920,14 +2169,20 @@ static const WgtMethodDescT sMethods[] = {
         { "Write",       WGT_SIG_STR,      (void *)wgtAnsiTermWriteString }
     };
     
    +// Bell: a BEL arrived.  Music: an ANSI music string (BASIC PLAY text).
    +static const WgtEventDescT sEvents[] = {
    +    { ANSITERM_EVT_BELL,  "" },
    +    { ANSITERM_EVT_MUSIC, "Macro As String" }
    +};
    +
     static const WgtIfaceT sIface = {
         .basName      = "Terminal",
         .props        = sProps,
         .propCount    = DVX_ARRAY_LEN(sProps),
         .methods      = sMethods,
         .methodCount  = DVX_ARRAY_LEN(sMethods),
    -    .events       = NULL,
    -    .eventCount   = 0,
    +    .events       = sEvents,
    +    .eventCount   = DVX_ARRAY_LEN(sEvents),
         .createSig    = WGT_CREATE_PARENT_INT_INT,
         .createArgs   = { ANSI_DEFAULT_COLS, ANSI_DEFAULT_ROWS }
     };
    diff --git a/src/widgets/kpunch/box/box.bhs b/src/widgets/kpunch/box/box.bhs
    index 388d3702..c5a40a32 100644
    --- a/src/widgets/kpunch/box/box.bhs
    +++ b/src/widgets/kpunch/box/box.bhs
    @@ -43,7 +43,7 @@ A container with a titled border. Child controls are placed inside the frame usi
     .table
       Property    Type      Description
       --------    -------   -------------------------------------------
    -  Alignment   Enum      Where the children sit along the frame from top to bottom when they do not fill it: Left (the top, the default), Center or Right (the bottom).
    +  Alignment   Enum      Where the children sit along the frame from top to bottom when they do not fill it: Left (the top, the default), Center, or Right (the bottom).
       Padding     Integer   Space in pixels between the border and the children: 4 unless set; 0 is none.
       Spacing     Integer   Space in pixels between one child and the next: none unless set.
     .endtable
    @@ -91,7 +91,7 @@ A container that arranges its children horizontally, left to right. No visible b
     .table
       Property    Type      Description
       --------    -------   -------------------------------------------
    -  Alignment   Enum      Where the children sit along the row when they do not fill it: Left (the left, the default), Center or Right (the right).
    +  Alignment   Enum      Where the children sit along the row when they do not fill it: Left (the left, the default), Center, or Right (the right).
       Padding     Integer   Space in pixels between the edge of the box and the children: 4 unless set; 0 is none.
       Spacing     Integer   Space in pixels between one child and the next: 4 unless set; 0 is none.
     .endtable
    @@ -137,7 +137,7 @@ A container that arranges its children vertically, top to bottom. No title or bo
     .table
       Property    Type      Description
       --------    -------   -------------------------------------------
    -  Alignment   Enum      Where the children sit along the column when they do not fill it: Left (the top, the default), Center or Right (the bottom).
    +  Alignment   Enum      Where the children sit along the column when they do not fill it: Left (the top, the default), Center, or Right (the bottom).
       Padding     Integer   Space in pixels between the edge of the box and the children: 4 unless set; 0 is none.
       Spacing     Integer   Space in pixels between one child and the next: 4 unless set; 0 is none.
     .endtable
    diff --git a/src/widgets/kpunch/canvas/canvas.bhs b/src/widgets/kpunch/canvas/canvas.bhs
    index 60f5dca3..c783eac2 100644
    --- a/src/widgets/kpunch/canvas/canvas.bhs
    +++ b/src/widgets/kpunch/canvas/canvas.bhs
    @@ -34,7 +34,7 @@
     
     VB Equivalent: PictureBox -- DVX Widget: canvas | Name Prefix: Picture
     
    -A drawing surface (canvas). Supports drawing lines, rectangles, circles, text, and individual pixels. Can load BMP, PNG, JPEG and GIF images and save BMP or PNG images. The default canvas size is 64x64 pixels.
    +A drawing surface (canvas). Supports drawing lines, rectangles, circles, text, and individual pixels. Can load BMP, PNG, JPEG, and GIF images and save BMP or PNG images. The default canvas size is 64x64 pixels.
     
     Colors are Visual Basic RGB values, &HBBGGRR, as RGB() makes them (RGB(255, 0, 0) or &HFF is red).
     
    @@ -61,8 +61,8 @@ Load and Save return 0 on success and -1 on failure (missing or unreadable file,
       DrawText x%, y%, text$                        Draw text at the given position.
       FillCircle cx%, cy%, radius%, color%          Draw a filled circle.
       FillRect x%, y%, w%, h%, color%               Draw a filled rectangle.
    -  GetPixel(x%, y%)                              Returns the color at a pixel. With fewer than 16 million colors on the screen it is the color the screen shows: black, white and full colors such as RGB(255, 0, 0) read back exactly, others may differ slightly.
    -  Load(path$)                                   Load a BMP, PNG, JPEG or GIF image; the canvas takes the image's size. Returns 0 on success, -1 on failure.
    +  GetPixel(x%, y%)                              Returns the color at a pixel. With fewer than 16 million colors on the screen it is the color the screen shows: black, white, and full colors such as RGB(255, 0, 0) read back exactly, others may differ slightly.
    +  Load(path$)                                   Load a BMP, PNG, JPEG, or GIF image; the canvas takes the image's size. Returns 0 on success, -1 on failure.
       Resize w%, h%                                 Resize the canvas to new dimensions. The drawing is cleared to white.
       Save(path$)                                   Save the canvas: BMP when path$ ends in .bmp, PNG otherwise. Returns 0 on success, -1 on failure.
       SetPenColor color%                            Set the drawing color for subsequent operations.
    diff --git a/src/widgets/kpunch/canvas/canvas.dhs b/src/widgets/kpunch/canvas/canvas.dhs
    index 3dbe090e..15fb42db 100644
    --- a/src/widgets/kpunch/canvas/canvas.dhs
    +++ b/src/widgets/kpunch/canvas/canvas.dhs
    @@ -86,7 +86,7 @@ WidgetT *cv = wgtCanvas(parent, 320, 200);
       void wgtCanvasResize(w, newW, newH)                                 Resize the canvas buffer and relayout. The old content is discarded; the new buffer is white.
       void wgtCanvasSetBorderStyle(w, style)                               CANVAS_BORDER_NONE (0) drops the sunken frame, any other value (CANVAS_BORDER_FIXED, 1, the default) shows it. The buffer keeps its size; the widget relays out.
       int32_t wgtCanvasGetBorderStyle(w)                                   CANVAS_BORDER_FIXED or CANVAS_BORDER_NONE.
    -  bool wgtCanvasGetDisplay(w, out)                                     Fill a DisplayT aimed at the canvas pixels (the screen's pixel format, clipped to the canvas), so any libdvx drawing call -- drawText, drawBevel, drawArrowGlyph, wmDrawScrollbarAt, rectCopyTransparent -- draws on the canvas with the context's blit ops; call wgtInvalidatePaint afterwards. False (out untouched) for a widget that is not a canvas. The BASIC DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow and PrintAccel methods use it.
    +  bool wgtCanvasGetDisplay(w, out)                                     Fill a DisplayT aimed at the canvas pixels (the screen's pixel format, clipped to the canvas), so any libdvx drawing call -- drawText, drawBevel, drawArrowGlyph, wmDrawScrollbarAt, rectCopyTransparent -- draws on the canvas with the context's blit ops; call wgtInvalidatePaint afterwards. False (out untouched) for a widget that is not a canvas. The BASIC DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, and PrintAccel methods use it.
     .endtable
     
     .h3 API Struct (wgtRegisterApi "canvas")
    @@ -139,8 +139,8 @@ Drawing state (pen color, pen size) is managed by the API functions above. The B
       DrawText x%, y%, text$                        Draw text at the given position.
       FillCircle cx%, cy%, radius%, color%          Fill a circle.
       FillRect x%, y%, w%, h%, color%               Fill a rectangle.
    -  GetPixel(x%, y%)                              Returns the color at a pixel (&HBBGGRR). With fewer than 16 million colors on the screen it is the color the screen shows: black, white and full colors such as RGB(255, 0, 0) read back exactly, others may differ slightly.
    -  Load(path$)                                   Load a BMP, PNG, JPEG or GIF image (the canvas takes its size). Returns 0 on success, -1 on error (WGT_SIG_RET_INT_STR).
    +  GetPixel(x%, y%)                              Returns the color at a pixel (&HBBGGRR). With fewer than 16 million colors on the screen it is the color the screen shows: black, white, and full colors such as RGB(255, 0, 0) read back exactly, others may differ slightly.
    +  Load(path$)                                   Load a BMP, PNG, JPEG, or GIF image (the canvas takes its size). Returns 0 on success, -1 on error (WGT_SIG_RET_INT_STR).
       Resize w%, h%                                 Resize the canvas (the drawing is cleared to white).
       Save(path$)                                   Save the canvas: BMP when path$ ends in .bmp, PNG otherwise. Returns 0 on success, -1 on error (WGT_SIG_RET_INT_STR).
       SetPenColor color%                            Set the drawing color (&HBBGGRR).
    diff --git a/src/widgets/kpunch/canvas/widgetCanvas.c b/src/widgets/kpunch/canvas/widgetCanvas.c
    index 7ca87ed6..3c75e482 100644
    --- a/src/widgets/kpunch/canvas/widgetCanvas.c
    +++ b/src/widgets/kpunch/canvas/widgetCanvas.c
    @@ -664,9 +664,10 @@ void wgtCanvasDrawRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t
     }
     
     
    -// Draw text with the context font in the pen color. The core drawText
    -// renders into a DisplayT aimed at the canvas buffer (same pixel format as
    -// the screen), so glyph handling matches every other text in the GUI.
    +// Draw text with the canvas's font (wgtFont) in the pen color. The core
    +// drawText renders into a DisplayT aimed at the canvas buffer (same pixel
    +// format as the screen), so glyph handling matches every other text in
    +// the GUI.
     void wgtCanvasDrawText(WidgetT *w, int32_t x, int32_t y, const char *text) {
         wgtCanvasDrawTextScaled(w, x, y, text, 1);
     }
    @@ -674,17 +675,14 @@ void wgtCanvasDrawText(WidgetT *w, int32_t x, int32_t y, const char *text) {
     
     // Draw text scale times as large (drawTextScaled) in the pen color.
     void wgtCanvasDrawTextScaled(WidgetT *w, int32_t x, int32_t y, const char *text, int32_t scale) {
    -    CanvasDataT *cd  = canvasData(w);
    -    AppContextT *ctx = cd ? wgtGetContext(w) : NULL;
    +    CanvasDataT *cd = canvasData(w);
    +    DisplayT     tmp;
     
    -    if (!ctx || !text) {
    +    if (!cd || !text || !wgtCanvasGetDisplay(w, &tmp)) {
             return;
         }
     
    -    DisplayT tmp;
    -
    -    wgtCanvasGetDisplay(w, &tmp);
    -    drawTextScaled(&tmp, cd->ops, &ctx->font, x, y, text, scale, cd->penColor);
    +    drawTextScaled(&tmp, cd->ops, wgtFont(w), x, y, text, scale, cd->penColor);
         wgtInvalidatePaint(w);
     }
     
    diff --git a/src/widgets/kpunch/comboBox/comboBox.dhs b/src/widgets/kpunch/comboBox/comboBox.dhs
    index fef73095..98053e36 100644
    --- a/src/widgets/kpunch/comboBox/comboBox.dhs
    +++ b/src/widgets/kpunch/comboBox/comboBox.dhs
    @@ -37,12 +37,14 @@
     
     .h2 ComboBox
     
    -A combination of a single-line text input and a drop-down list. The user can type a value or select one from the list. When the user picks a list item, its text is copied into the edit buffer. Supports full text editing (cursor movement, selection, clipboard, undo) via the texthelp library and a popup overlay list via the listhelp library. Setting the text or picking a list item clears the undo history. Keyboard: Down, F4 or Alt+Down/Alt+Up opens the list; while it is open Down/Up move the highlight and Enter, F4 or Alt+Down/Alt+Up pick the item and close it (Esc closes without picking).
    +A combination of a single-line text input and a drop-down list. The user can type a value or select one from the list. When the user picks a list item, its text is copied into the edit buffer. Supports full text editing (cursor movement, selection, clipboard, undo) via the texthelp library and a popup overlay list via the listhelp library. Setting the text or picking a list item clears the undo history. Keyboard: Down, F4, or Alt+Down/Alt+Up opens the list; while it is open Down/Up move the highlight and Enter, F4, or Alt+Down/Alt+Up pick the item and close it (Esc closes without picking).
     
    -A read-only combo box (wgtSetReadOnly) ignores the editing keys (typing, Backspace, Delete, paste, cut, undo) but still allows cursor movement, selection, copy and picking an item from the list.
    +A read-only combo box (wgtSetReadOnly) ignores the editing keys (typing, Backspace, Delete, paste, cut, undo) but still allows cursor movement, selection, copy, and picking an item from the list.
     
     Depends on "texthelp" and "listhelp" helper libraries (declared in combobox.dep).
     
    +WCLASS_FIXED_FONT: it edits by columns, so its font must be fixed-pitch. Any size works (the column math uses the font's charWidth and charHeight); a proportional font set with wgtSetFont, or inherited, is replaced by the system font when it draws.
    +
     Header: widgets/comboBox.h
     
     .h3 Creation
    diff --git a/src/widgets/kpunch/comboBox/combobox.bhs b/src/widgets/kpunch/comboBox/combobox.bhs
    index 0b809fba..1671b877 100644
    --- a/src/widgets/kpunch/comboBox/combobox.bhs
    +++ b/src/widgets/kpunch/comboBox/combobox.bhs
    @@ -31,6 +31,8 @@ VB Equivalent: ComboBox -- DVX Widget: combobox (editable text field + drop-down
     
     A combination of a single-line text input and a drop-down list. The user can either type a value or pick one from the list. Selecting a list item copies its text into the edit field. Supports the same AddItem / RemoveItem / Clear / List methods as ListBox.
     
    +Its font must be fixed-pitch, every character the same width; the designer's Font dialog offers only those families for it. Given a proportional font it draws in the system font instead.
    +
     .h2 Type-Specific Properties
     
     .table
    diff --git a/src/widgets/kpunch/comboBox/widgetComboBox.c b/src/widgets/kpunch/comboBox/widgetComboBox.c
    index fcdb4157..cddb9c48 100644
    --- a/src/widgets/kpunch/comboBox/widgetComboBox.c
    +++ b/src/widgets/kpunch/comboBox/widgetComboBox.c
    @@ -280,10 +280,10 @@ const char *widgetComboBoxGetText(const WidgetT *w) {
     static void widgetComboBoxOnDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
         (void)root;
         (void)vy;
    -    ComboBoxDataT *d   = (ComboBoxDataT *)w->data;
    -    AppContextT   *ctx = wgtGetContext(w);
    +    ComboBoxDataT     *d    = (ComboBoxDataT *)w->data;
    +    const BitmapFontT *font = wgtFont(w);
     
    -    widgetTextEditDragUpdateLine(&d->te, vx, w->x + TEXT_INPUT_PAD, comboBoxVisibleChars(w, &ctx->font), &ctx->font);
    +    widgetTextEditDragUpdateLine(&d->te, vx, w->x + TEXT_INPUT_PAD, comboBoxVisibleChars(w, font), font);
     }
     
     
    @@ -335,6 +335,7 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
     
     
     void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
    +    (void)root;
         sFocusedWidget = w;
         ComboBoxDataT *d = (ComboBoxDataT *)w->data;
         ListPopupT    *p = &d->popup;
    @@ -359,10 +360,8 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
         }
     
         // Text area click  -- focus for editing
    -    AppContextT *ctx = (AppContextT *)root->userData;
    -
         clearOtherSelections(w);
    -    widgetTextEditMouseClick(w, &d->te, vx, vy, w->x + TEXT_INPUT_PAD, &ctx->font, true, true);
    +    widgetTextEditMouseClick(w, &d->te, vx, vy, w->x + TEXT_INPUT_PAD, wgtFont(w), true, true);
     }
     
     
    @@ -411,7 +410,7 @@ void widgetComboBoxSetText(WidgetT *w, const char *text) {
     
     static const WidgetClassT sClassComboBox = {
         .version = WGT_CLASS_VERSION,
    -    .flags   = WCLASS_FOCUSABLE | WCLASS_HAS_POPUP | WCLASS_SCROLLABLE,
    +    .flags   = WCLASS_FOCUSABLE | WCLASS_HAS_POPUP | WCLASS_SCROLLABLE | WCLASS_FIXED_FONT,
         .handlers = {
             [WGT_METHOD_PAINT]             = (void *)widgetComboBoxPaint,
             [WGT_METHOD_PAINT_OVERLAY]     = (void *)widgetComboBoxPaintPopup,
    diff --git a/src/widgets/kpunch/dataCtrl/dataCtrl.dhs b/src/widgets/kpunch/dataCtrl/dataCtrl.dhs
    index fff92892..0a53b297 100644
    --- a/src/widgets/kpunch/dataCtrl/dataCtrl.dhs
    +++ b/src/widgets/kpunch/dataCtrl/dataCtrl.dhs
    @@ -149,4 +149,4 @@ Set properties (DatabaseName, RecordSource, KeyColumn, etc.) and then call dataC
       Validate       Fires before writing. Return false to cancel the write.
     .endtable
     
    -While the Validate handler runs, the moves (MoveFirst/MoveLast/MoveNext/MovePrevious, dataCtrlSetCurrentRow), AddNew, Refresh and Update on the same control are refused and return without doing anything: each would save, and therefore validate, the same row again. Delete is still allowed; Update re-checks the row after the handler returns.
    +While the Validate handler runs, the moves (MoveFirst/MoveLast/MoveNext/MovePrevious, dataCtrlSetCurrentRow), AddNew, Refresh, and Update on the same control are refused and return without doing anything: each would save, and therefore validate, the same row again. Delete is still allowed; Update re-checks the row after the handler returns.
    diff --git a/src/widgets/kpunch/dataCtrl/datactrl.bhs b/src/widgets/kpunch/dataCtrl/datactrl.bhs
    index 84a0c7b3..42b0acf5 100644
    --- a/src/widgets/kpunch/dataCtrl/datactrl.bhs
    +++ b/src/widgets/kpunch/dataCtrl/datactrl.bhs
    @@ -39,7 +39,7 @@
     
     VB Equivalent: Data -- DVX Widget: data | Name Prefix: Data
     
    -A data access control that connects to a SQLite database and provides record navigation. Other controls bind to a Data control via their DataSource and DataField properties. The Data control cache is populated by calling Refresh. Move operations and Refresh auto-save the current row if dirty; a move is refused while the save fails, and Refresh discards an edit that cannot be saved. Updates find the record by the key value it was loaded with, so the key column itself can be edited; a key stored as a number in a column without a declared type, or a missing (NULL) key, is still found as long as it identifies exactly one record. Writing a field back with the value it already has is not an edit and does not fire Validate. While a Validate handler runs, MoveFirst, MoveLast, MoveNext, MovePrevious, AddNew, Refresh and Update on the same Data control are ignored (they would save, and validate, the same record again); Delete is allowed. A RecordSource that is a SELECT query is read-only, and edits made to its records are dropped when the current record changes.
    +A data access control that connects to a SQLite database and provides record navigation. Other controls bind to a Data control via their DataSource and DataField properties. The Data control cache is populated by calling Refresh. Move operations and Refresh auto-save the current row if dirty; a move is refused while the save fails, and Refresh discards an edit that cannot be saved. Updates find the record by the key value it was loaded with, so the key column itself can be edited; a key stored as a number in a column without a declared type, or a missing (NULL) key, is still found as long as it identifies exactly one record. Writing a field back with the value it already has is not an edit and does not fire Validate. While a Validate handler runs, MoveFirst, MoveLast, MoveNext, MovePrevious, AddNew, Refresh, and Update on the same Data control are ignored (they would save, and validate, the same record again); Delete is allowed. A RecordSource that is a SELECT query is read-only, and edits made to its records are dropped when the current record changes.
     
     .link ctrl.databinding See Data Binding for details
     
    diff --git a/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c b/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c
    index 867c97b2..e6217a48 100644
    --- a/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c
    +++ b/src/widgets/kpunch/dataCtrl/widgetDataCtrl.c
    @@ -49,13 +49,14 @@ DVX_ABI_STAMP(datactrl);
     // Constants
     // ============================================================
     
    -#define DATA_HEIGHT         24
    +#define DATA_HEIGHT         24    // minimum height, for the navigation chevrons
     #define DATA_BTN_W          20
     #define DATA_NAV_BTNS       4     // |< < > >|
     #define DATA_BORDER         2
     #define DATA_BTN_BEVEL      1
     #define DATA_CAPTION_MIN_W  60    // caption space in the minimum width
     #define DATA_CAPTION_PAD    4     // gap between the buttons and the caption
    +#define DATA_CAPTION_PAD_V  2     // rows above and below the caption inside the border
     #define DATA_GLYPH_ARM      4     // rows in each arm of a navigation chevron
     #define DATA_GLYPH_BAR_GAP  2     // chevron tip to the |< / >| bar
     #define DATA_GLYPH_TIP_OFF  1     // |< / >| tip shift that centres chevron + bar
    @@ -303,10 +304,11 @@ static bool dataCtrlBasGetField(WidgetT *w, const WgtValueT *args, int32_t argc,
     }
     
     
    +// At least DATA_HEIGHT high for the navigation chevrons, and higher when
    +// the font's line and DATA_CAPTION_PAD_V above and below it need more.
     static void dataCtrlCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    -    (void)font;
         w->calcMinW = DATA_BTN_W * DATA_NAV_BTNS + DATA_CAPTION_MIN_W;
    -    w->calcMinH = DATA_HEIGHT;
    +    w->calcMinH = DVX_MAX(DATA_HEIGHT, font->charHeight + (DATA_BORDER + DATA_CAPTION_PAD_V) * 2);
     }
     
     
    diff --git a/src/widgets/kpunch/dbGrid/widgetDbGrid.c b/src/widgets/kpunch/dbGrid/widgetDbGrid.c
    index 6ecb01aa..b45cd390 100644
    --- a/src/widgets/kpunch/dbGrid/widgetDbGrid.c
    +++ b/src/widgets/kpunch/dbGrid/widgetDbGrid.c
    @@ -105,38 +105,39 @@ typedef struct {
     // ============================================================
     
     typedef struct {
    -    WidgetT      *dataWidget;   // the Data control (set by form runtime)
    +    WidgetT           *dataWidget;   // the Data control (set by form runtime)
     
         // Column configuration -- heap-allocated, sized to colCount
    -    DbGridColT   *columns;
    -    int32_t       colCount;
    -    int32_t       colCap;        // allocated capacity
    +    DbGridColT        *columns;
    +    int32_t            colCount;
    +    int32_t            colCap;       // allocated capacity
     
         // Display state
    -    int32_t       scrollPos;    // vertical scroll (row units)
    -    int32_t       scrollPosH;   // horizontal scroll (pixel units)
    -    int32_t      *resolvedW;     // heap-allocated, sized to colCap
    -    int32_t       totalColW;    // 0 = needs recalc
    +    int32_t            scrollPos;    // vertical scroll (row units)
    +    int32_t            scrollPosH;   // horizontal scroll (pixel units)
    +    int32_t           *resolvedW;    // heap-allocated, sized to colCap
    +    int32_t            totalColW;    // 0 = needs recalc
    +    FontStampT         colWFont;     // font resolvedW was resolved in
     
         // Sort state
    -    int32_t       sortCol;      // data-column index (-1 = none)
    -    ListViewSortE sortDir;
    -    int32_t      *sortIndex;    // display-row -> data-row mapping (NULL = natural)
    -    int32_t       sortCount;    // length of sortIndex
    -    uint32_t      dataGen;      // Data generation the widths and sort were built for
    +    int32_t            sortCol;      // data-column index (-1 = none)
    +    ListViewSortE      sortDir;
    +    int32_t           *sortIndex;    // display-row -> data-row mapping (NULL = natural)
    +    int32_t            sortCount;    // length of sortIndex
    +    uint32_t           dataGen;      // Data generation the widths and sort were built for
     
         // Column resize drag state
    -    int32_t       resizeCol;
    -    int32_t       resizeStartX;
    -    int32_t       resizeOrigW;
    +    int32_t            resizeCol;
    +    int32_t            resizeStartX;
    +    int32_t            resizeOrigW;
     
         // Scrollbar drag state
    -    int32_t       sbDragOrient; // DBGRID_DRAG_*
    -    int32_t       sbDragOff;
    +    int32_t            sbDragOrient; // DBGRID_DRAG_*
    +    int32_t            sbDragOff;
     
         // Flags (grouped last to minimize struct padding)
    -    bool          gridLines;
    -    bool          resizeDragging;
    +    bool               gridLines;
    +    bool               resizeDragging;
     } DbGridDataT;
     
     // One sort key: the cell text, its data row and the direction sign, so
    @@ -421,11 +422,12 @@ static int32_t dbGridGetCursorShape(const WidgetT *w, int32_t mx, int32_t my) {
             return CURSOR_RESIZE_H;
         }
     
    -    int32_t headerH = ctx->font.charHeight + DBGRID_HEADER_VPAD;
    -    int32_t headerTop = w->y + DBGRID_BORDER;
    +    const BitmapFontT *font      = wgtFontFor(w, &ctx->font);
    +    int32_t            headerH   = font->charHeight + DBGRID_HEADER_VPAD;
    +    int32_t            headerTop = w->y + DBGRID_BORDER;
     
         if (my >= headerTop && my < headerTop + headerH) {
    -        resolveColumns(w, &ctx->font);
    +        resolveColumns(w, font);
     
             if (colBorderHit(w, mx) >= 0) {
                 return CURSOR_RESIZE_H;
    @@ -505,7 +507,7 @@ static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y)
             int32_t visRows;
             bool    needV;
             bool    needH;
    -        computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
    +        computeLayout(w, wgtFontFor(w, &ctx->font), &innerW, &innerH, &headerH, &visRows, &needV, &needH);
     
             int32_t rowCount = getDataRowCount(d);
             int32_t maxScroll = rowCount - visRows;
    @@ -538,7 +540,7 @@ static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y)
             int32_t visRows;
             bool    needV;
             bool    needH;
    -        computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
    +        computeLayout(w, wgtFontFor(w, &ctx->font), &innerW, &innerH, &headerH, &visRows, &needV, &needH);
     
             int32_t maxScroll = d->totalColW - innerW;
     
    @@ -578,7 +580,7 @@ static void dbGridOnKey(WidgetT *w, int32_t key, int32_t mod) {
         int32_t visRows;
         bool    needV;
         bool    needH;
    -    computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
    +    computeLayout(w, wgtFontFor(w, &ctx->font), &innerW, &innerH, &headerH, &visRows, &needV, &needH);
     
         int32_t dispRow = dataRowToDisplay(d, currentDataRow(d));
         int32_t newDisp = dispRow;
    @@ -680,15 +682,16 @@ static void dbGridOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
     
         sFocusedWidget = w;
     
    -    int32_t innerW;
    -    int32_t innerH;
    -    int32_t headerH;
    -    int32_t visRows;
    -    bool    needV;
    -    bool    needH;
    -    computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
    +    const BitmapFontT *font = wgtFontFor(w, &ctx->font);
    +    int32_t            innerW;
    +    int32_t            innerH;
    +    int32_t            headerH;
    +    int32_t            visRows;
    +    bool               needV;
    +    bool               needH;
    +    computeLayout(w, font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
     
    -    int32_t rowH      = ctx->font.charHeight + DBGRID_ROW_VPAD;
    +    int32_t rowH      = font->charHeight + DBGRID_ROW_VPAD;
         int32_t headerTop = w->y + DBGRID_BORDER;
         int32_t dataTop   = headerTop + headerH;
         int32_t rowCount  = getDataRowCount(d);
    @@ -726,7 +729,7 @@ static void dbGridOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
     
         // Column header area
         if (vy >= headerTop && vy < dataTop) {
    -        resolveColumns(w, &ctx->font);
    +        resolveColumns(w, font);
     
             // Check column border for resize
             int32_t borderCol = colBorderHit(w, vx);
    @@ -805,7 +808,7 @@ static bool dbGridOnWheel(WidgetT *w, int32_t delta) {
         bool    needV;
         bool    needH;
     
    -    computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
    +    computeLayout(w, wgtFontFor(w, &ctx->font), &innerW, &innerH, &headerH, &visRows, &needV, &needH);
     
         int32_t maxScroll = DVX_MAX(getDataRowCount(d) - visRows, 0);
         int32_t newPos    = clampInt(d->scrollPos + delta * ctx->wheelStep, 0, maxScroll);
    @@ -1255,11 +1258,12 @@ static int32_t getDataRowCount(const DbGridDataT *d) {
     }
     
     
    -// resolveColumns -- compute pixel widths for each column
    +// resolveColumns -- compute pixel widths for each column (again after a
    +// data or column change, or in another font)
     static void resolveColumns(const WidgetT *w, const BitmapFontT *font) {
         DbGridDataT *d = (DbGridDataT *)w->data;
     
    -    if (d->totalColW > 0) {
    +    if (d->totalColW > 0 && fontStampIs(&d->colWFont, font)) {
             return;
         }
     
    @@ -1298,6 +1302,8 @@ static void resolveColumns(const WidgetT *w, const BitmapFontT *font) {
     
             d->totalColW += d->resolvedW[c];
         }
    +
    +    fontStampSet(&d->colWFont, font);
     }
     
     
    diff --git a/src/widgets/kpunch/dropdown/dropdown.bhs b/src/widgets/kpunch/dropdown/dropdown.bhs
    index e4daa9a1..c9593bc1 100644
    --- a/src/widgets/kpunch/dropdown/dropdown.bhs
    +++ b/src/widgets/kpunch/dropdown/dropdown.bhs
    @@ -29,7 +29,7 @@
     
     DVX Extension -- DVX Widget: dropdown (non-editable drop-down list)
     
    -A read-only drop-down list. Unlike ComboBox, the user cannot type free text; they can only select from the provided items. Supports AddItem / RemoveItem / Clear / List. Use the Up and Down keys to change the choice. F4, Alt+Down, Space or Enter opens the list; in the open list use the arrow keys to move and Enter (or F4, Alt+Down) to pick. Esc closes the list without changing the choice.
    +A read-only drop-down list. Unlike ComboBox, the user cannot type free text; they can only select from the provided items. Supports AddItem / RemoveItem / Clear / List. Use the Up and Down keys to change the choice. F4, Alt+Down, Space, or Enter opens the list; in the open list use the arrow keys to move and Enter (or F4, Alt+Down) to pick. Esc closes the list without changing the choice.
     
     .h2 Type-Specific Properties
     
    diff --git a/src/widgets/kpunch/dropdown/dropdown.dhs b/src/widgets/kpunch/dropdown/dropdown.dhs
    index 27fef444..d26697e2 100644
    --- a/src/widgets/kpunch/dropdown/dropdown.dhs
    +++ b/src/widgets/kpunch/dropdown/dropdown.dhs
    @@ -35,7 +35,7 @@
     
     .h2 Dropdown
     
    -A drop-down list that displays a single selected item and expands to a popup list when clicked. Unlike ComboBox, the user cannot type free text -- only list items can be selected. Keyboard: while closed, Down/Up change the selection and type-ahead picks the next item starting with a printable key; Space, Enter, F4 or Alt+Down/Alt+Up open the list. While open, Down/Up move the highlight and Enter, Space, F4 or Alt+Down/Alt+Up select it and close the list (Esc closes without selecting). Ctrl and Alt chords never change the selection. The popup overlay is rendered above all other widgets via the widget paint overlay mechanism.
    +A drop-down list that displays a single selected item and expands to a popup list when clicked. Unlike ComboBox, the user cannot type free text -- only list items can be selected. Keyboard: while closed, Down/Up change the selection and type-ahead picks the next item starting with a printable key; Space, Enter, F4, or Alt+Down/Alt+Up open the list. While open, Down/Up move the highlight and Enter, Space, F4, or Alt+Down/Alt+Up select it and close the list (Esc closes without selecting). Ctrl and Alt chords never change the selection. The popup overlay is rendered above all other widgets via the widget paint overlay mechanism.
     
     Depends on the "listhelp" helper library (declared in dropdown.dep).
     
    diff --git a/src/widgets/kpunch/helpView/helpView.dhs b/src/widgets/kpunch/helpView/helpView.dhs
    index 75ce8fc1..5bd6ce44 100644
    --- a/src/widgets/kpunch/helpView/helpView.dhs
    +++ b/src/widgets/kpunch/helpView/helpView.dhs
    @@ -34,9 +34,9 @@
     
     .h2 HelpView
     
    -One topic of a compiled help file, shown the way the DVX Help Viewer shows it -- the Help Viewer itself uses this widget. Headings (three levels), word-wrapped paragraphs and list items, notes (info, tip and warning), code blocks and tables (preformatted), pictures (left, centered or right), horizontal rules and links, in a scrolling pane, in the theme's colours. Text re-wraps when the widget changes width.
    +One topic of a compiled help file, shown the way the DVX Help Viewer shows it -- the Help Viewer itself uses this widget. Headings (three levels), word-wrapped paragraphs and list items, notes (info, tip, and warning), code blocks and tables (preformatted), pictures (left, centered, or right), horizontal rules and links, in a scrolling pane, in the theme's colours. Text re-wraps when the widget changes width.
     
    -Links take the keyboard focus (Tab) and are followed with a click, Enter or Space: the widget keeps the link's target topic ID (wgtHelpViewGetLinkTarget) and calls the link callback with it. The widget does not change topic by itself; the callback usually finds the topic (hlpFileFindTopic) and shows it, which may rebuild the pane from inside the link's own handler. Everything a topic shows is copied when it is shown, so the help file may be closed afterwards.
    +Links take the keyboard focus (Tab) and are followed with a click, Enter, or Space: the widget keeps the link's target topic ID (wgtHelpViewGetLinkTarget) and calls the link callback with it. The widget does not change topic by itself; the callback usually finds the topic (hlpFileFindTopic) and shows it, which may rebuild the pane from inside the link's own handler. Everything a topic shows is copied when it is shown, so the help file may be closed afterwards.
     
     The help file is read with the help file library (hlpfile/hlpFile.h).
     
    diff --git a/src/widgets/kpunch/helpView/helpview.bhs b/src/widgets/kpunch/helpView/helpview.bhs
    index 4182798d..16e18b29 100644
    --- a/src/widgets/kpunch/helpView/helpview.bhs
    +++ b/src/widgets/kpunch/helpView/helpview.bhs
    @@ -34,7 +34,7 @@
     
     VB Equivalent: (none) -- DVX Widget: helpview
     
    -Shows one topic of a help file the way the DVX Help Viewer does: headings, paragraphs and lists wrapped to the control's width, notes, code, tables, pictures, rules and links, with a scroll bar when the topic is longer than the control. Open the help file with hlpfile.bas (HlpOpen) and give its handle to ShowTopic. With a TreeView for the table of contents and a few buttons, a HelpPane makes a complete help viewer (see Help Files under Include Libraries).
    +Shows one topic of a help file the way the DVX Help Viewer does: headings, paragraphs, and lists wrapped to the control's width, notes, code, tables, pictures, rules, and links, with a scroll bar when the topic is longer than the control. Open the help file with hlpfile.bas (HlpOpen) and give its handle to ShowTopic. With a TreeView for the table of contents and a few buttons, a HelpPane makes a complete help viewer (see Help Files under Include Libraries).
     
     A link is followed with a click, or with Enter or Space after Tab has moved to it: the HelpPane fires LinkClick with the ID of the topic the link names and remembers it in LinkTopic. It does not change topic by itself -- the LinkClick event procedure usually shows the topic and adds it to its Back list. Everything a topic shows is copied when it is shown, so closing the help file afterwards does not change what the HelpPane shows.
     
    diff --git a/src/widgets/kpunch/helpView/widgetHelpView.c b/src/widgets/kpunch/helpView/widgetHelpView.c
    index f4ea5c7c..6957e130 100644
    --- a/src/widgets/kpunch/helpView/widgetHelpView.c
    +++ b/src/widgets/kpunch/helpView/widgetHelpView.c
    @@ -96,7 +96,7 @@ typedef struct {
         char    *text;       // original flowing text (no newlines)
         char    *wrapped;    // cached wrapped text (with \n), NULL = needs wrap
         int32_t  lineCount;  // line count of wrapped text
    -    int32_t  wrapWidth;  // pixel width text was wrapped at (-1 = dirty)
    +    int32_t  wrapCols;   // columns text was wrapped at (-1 = dirty)
         uint8_t  noteType;   // HLP_NOTE_* (only notes read it)
     } HelpWrapDataT;
     
    @@ -193,7 +193,7 @@ static const WidgetClassT sClassHelpView = {
     
     static const WidgetClassT sClassHelpText = {
         .version  = WGT_CLASS_VERSION,
    -    .flags    = 0,
    +    .flags    = WCLASS_FIXED_FONT,
         .handlers = {
             [WGT_METHOD_PAINT]         = (void *)helpTextPaint,
             [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpTextCalcMinSize,
    @@ -203,7 +203,7 @@ static const WidgetClassT sClassHelpText = {
     
     static const WidgetClassT sClassHelpHeading = {
         .version  = WGT_CLASS_VERSION,
    -    .flags    = 0,
    +    .flags    = WCLASS_FIXED_FONT,
         .handlers = {
             [WGT_METHOD_PAINT]         = (void *)helpHeadingPaint,
             [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpHeadingCalcMinSize,
    @@ -213,7 +213,7 @@ static const WidgetClassT sClassHelpHeading = {
     
     static const WidgetClassT sClassHelpLink = {
         .version  = WGT_CLASS_VERSION,
    -    .flags    = WCLASS_FOCUSABLE,
    +    .flags    = WCLASS_FOCUSABLE | WCLASS_FIXED_FONT,
         .handlers = {
             [WGT_METHOD_PAINT]         = (void *)helpLinkPaint,
             [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpLinkCalcMinSize,
    @@ -225,7 +225,7 @@ static const WidgetClassT sClassHelpLink = {
     
     static const WidgetClassT sClassHelpNote = {
         .version  = WGT_CLASS_VERSION,
    -    .flags    = 0,
    +    .flags    = WCLASS_FIXED_FONT,
         .handlers = {
             [WGT_METHOD_PAINT]         = (void *)helpNotePaint,
             [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpNoteCalcMinSize,
    @@ -235,7 +235,7 @@ static const WidgetClassT sClassHelpNote = {
     
     static const WidgetClassT sClassHelpCode = {
         .version  = WGT_CLASS_VERSION,
    -    .flags    = 0,
    +    .flags    = WCLASS_FIXED_FONT,
         .handlers = {
             [WGT_METHOD_PAINT]         = (void *)helpCodePaint,
             [WGT_METHOD_CALC_MIN_SIZE] = (void *)helpCodeCalcMinSize,
    @@ -354,8 +354,8 @@ static bool buildRecord(const HlpRecordHdrT *hdr, const char *payload, void *use
                         wd->text = strdup(payload);
                     }
     
    -                wd->wrapWidth = -1;
    -                wd->noteType  = hdr->flags;
    +                wd->wrapCols = -1;
    +                wd->noteType = hdr->flags;
                 }
     
                 attachContentData(w, wd, wd && wd->text, helpWrapDestroy);
    @@ -457,17 +457,19 @@ static int32_t countLines(const char *text) {
     }
     
     
    -// Wraps the widget's text at the given pixel width minus padPixels and
    -// caches the result (wrapped, lineCount) -- only re-wraps when pixelW
    -// changes.
    +// Wraps the widget's text at the columns of charW pixels that fit in
    +// pixelW minus padPixels and caches the result (wrapped, lineCount) --
    +// only re-wraps when that column count changes (a new width or font).
     static void doWrap(HelpWrapDataT *wd, int32_t pixelW, int32_t charW, int32_t padPixels) {
    -    if (wd->wrapped && wd->wrapWidth == pixelW) {
    +    int32_t cols = (pixelW - padPixels) / charW;
    +
    +    if (wd->wrapped && wd->wrapCols == cols) {
             return;
         }
     
         free(wd->wrapped);
    -    wd->wrapped   = hlpWrapText(wd->text, (pixelW - padPixels) / charW);
    -    wd->wrapWidth = pixelW;
    +    wd->wrapped   = hlpWrapText(wd->text, cols);
    +    wd->wrapCols  = cols;
         wd->lineCount = countLines(wd->wrapped ? wd->wrapped : wd->text);
     }
     
    diff --git a/src/widgets/kpunch/image/image.bhs b/src/widgets/kpunch/image/image.bhs
    index ce4cdc36..b8d5ee23 100644
    --- a/src/widgets/kpunch/image/image.bhs
    +++ b/src/widgets/kpunch/image/image.bhs
    @@ -40,7 +40,7 @@ A static image display control. Loads bitmap images from file. Set the Picture p
     .table
       Property      Type      Description
       -----------   -------   -------------------------------------------
    -  Picture       Picture   Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. A disabled Image is drawn grayed (its transparent color stays transparent).
    +  Picture       Picture   Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget, or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. A disabled Image is drawn grayed (its transparent color stays transparent).
       Stretch       Boolean   When True, an image larger than the control is scaled down to fit, keeping its proportions; a smaller image stays at natural size. When False, the image is shown at natural size.
       ImageWidth    Integer   Width of the loaded image in pixels (read-only).
       ImageHeight   Integer   Height of the loaded image in pixels (read-only).
    diff --git a/src/widgets/kpunch/imageButton/imgbtn.bhs b/src/widgets/kpunch/imageButton/imgbtn.bhs
    index fb8c4385..78ee611f 100644
    --- a/src/widgets/kpunch/imageButton/imgbtn.bhs
    +++ b/src/widgets/kpunch/imageButton/imgbtn.bhs
    @@ -36,7 +36,7 @@ A button that displays an image instead of text. Has the same press/release beha
     .table
       Property      Type      Description
       -----------   -------   -------------------------------------------
    -  Picture       Picture   Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged.
    +  Picture       Picture   Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget, or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged.
       ImageWidth    Integer   Width of the loaded image in pixels (read-only).
       ImageHeight   Integer   Height of the loaded image in pixels (read-only).
     .endtable
    diff --git a/src/widgets/kpunch/listBox/widgetListBox.c b/src/widgets/kpunch/listBox/widgetListBox.c
    index e8c33620..5fcddff1 100644
    --- a/src/widgets/kpunch/listBox/widgetListBox.c
    +++ b/src/widgets/kpunch/listBox/widgetListBox.c
    @@ -28,8 +28,9 @@
     // ListItemsT in listhelp adopts the caller's array first). A vertical
     // scrollbar appears when the item count exceeds visible rows, a horizontal
     // one when the widest item is wider than the box (its width capped below
    -// what the items ask for); that one scrolls by whole characters, and so do
    -// Left and Right.
    +// what the items ask for); that one scrolls by columns the font's
    +// charWidth wide (whole characters of a fixed-pitch font), and so do Left
    +// and Right.  Only the characters that show whole are drawn, in any font.
     //
     // Multi-select uses a parallel bits array (one byte per item, 0 or 1)
     // rather than a bitfield. Using a full byte per item wastes some memory but
    @@ -80,7 +81,7 @@ typedef struct {
         ListItemsT       items;
         ListSelT         sel;
         int32_t          scrollPos;     // first row shown
    -    int32_t          scrollCol;     // first character shown
    +    int32_t          scrollCol;     // first column shown (font->charWidth pixels each)
         int32_t          dragIdx;
         int32_t          dropIdx;
         ScrollbarOrientE sbDragOrient;
    @@ -89,13 +90,14 @@ typedef struct {
     } ListBoxDataT;
     
     // Where a list box's parts are (listBoxGeom): the area inside the border
    -// less the scroll bars, the whole rows and characters that fit in it, and
    -// how far each axis scrolls.
    +// less the scroll bars, the whole rows and columns that fit in it, the
    +// columns the widest item takes, and how far each axis scrolls.
     typedef struct {
         int32_t innerW;
         int32_t innerH;
         int32_t visibleRows;
         int32_t visibleCols;
    +    int32_t totalCols;
         int32_t maxScroll;
         int32_t maxScrollCol;
         bool    needV;
    @@ -146,9 +148,10 @@ static void        widgetListBoxScrollDragUpdate(WidgetT *w, ScrollbarOrientE or
     // array directly and the widget is refreshed once.
     static void basSetItems(WidgetT *w, const char *spec) {
         VALIDATE_WIDGET_VOID(w, sTypeId);
    -    ListBoxDataT *d            = (ListBoxDataT *)w->data;
    -    bool          widthChanged = listItemsClear(&d->items);
    -    char         *buf          = (spec && *spec) ? strdup(spec) : NULL;
    +    ListBoxDataT      *d            = (ListBoxDataT *)w->data;
    +    const BitmapFontT *font         = wgtFont(w);
    +    bool               widthChanged = listItemsClear(&d->items);
    +    char              *buf          = (spec && *spec) ? strdup(spec) : NULL;
     
         for (char *tok = buf; tok; ) {
             char *sep = strchr(tok, '|');
    @@ -157,7 +160,7 @@ static void basSetItems(WidgetT *w, const char *spec) {
                 *sep = '\0';
             }
     
    -        if (listItemsAdd(&d->items, tok)) {
    +        if (listItemsAdd(&d->items, font, tok)) {
                 widthChanged = true;
             }
     
    @@ -178,11 +181,10 @@ static void ensureScrollVisible(WidgetT *w, int32_t idx) {
             return;
         }
     
    -    ListBoxDataT *d   = (ListBoxDataT *)w->data;
    -    AppContextT  *ctx = wgtGetContext(w);
    +    ListBoxDataT *d = (ListBoxDataT *)w->data;
         ListBoxGeomT  g;
     
    -    listBoxGeom(w, &ctx->font, &g);
    +    listBoxGeom(w, wgtFont(w), &g);
     
         int32_t visibleRows = DVX_MAX(g.visibleRows, 1);
     
    @@ -196,16 +198,20 @@ static void ensureScrollVisible(WidgetT *w, int32_t idx) {
     
     // The scroll bars each one needs (widgetScrollbarNeeds: the items' widest
     // text plus both paddings, and their rows), and what is left for the items.
    +// The widest item's columns are rounded up, so scrolling to the last one
    +// shows its last character whole.
     static void listBoxGeom(const WidgetT *w, const BitmapFontT *font, ListBoxGeomT *g) {
    -    const ListBoxDataT *d = (const ListBoxDataT *)w->data;
    +    ListBoxDataT *d      = (ListBoxDataT *)w->data;
    +    int32_t       itemsW = listItemsMaxWidth(&d->items, font);
     
         g->innerW = w->w - LISTBOX_BORDER * 2;
         g->innerH = w->h - LISTBOX_BORDER * 2;
    -    widgetScrollbarNeeds(d->items.maxLen * font->charWidth + LISTBOX_PAD * 2, d->items.count * font->charHeight, &g->innerW, &g->innerH, &g->needV, &g->needH);
    +    widgetScrollbarNeeds(itemsW + LISTBOX_PAD * 2, d->items.count * font->charHeight, &g->innerW, &g->innerH, &g->needV, &g->needH);
         g->visibleRows  = DVX_MAX(g->innerH / font->charHeight, 0);
         g->visibleCols  = DVX_MAX((g->innerW - LISTBOX_PAD * 2) / font->charWidth, 0);
    +    g->totalCols    = (itemsW + font->charWidth - 1) / font->charWidth;
         g->maxScroll    = DVX_MAX(d->items.count - g->visibleRows, 0);
    -    g->maxScrollCol = DVX_MAX(d->items.maxLen - g->visibleCols, 0);
    +    g->maxScrollCol = DVX_MAX(g->totalCols - g->visibleCols, 0);
     }
     
     
    @@ -259,7 +265,7 @@ void wgtListBoxAddItem(WidgetT *w, const char *text) {
         ListBoxDataT *d = (ListBoxDataT *)w->data;
         // Items handed in through SetItems are adopted first so they survive
         // the append instead of being replaced by the owned array.
    -    bool widthChanged = listItemsAdd(&d->items, text);
    +    bool widthChanged = listItemsAdd(&d->items, wgtFont(w), text);
     
         listSelInsert(&d->sel, d->items.count, d->items.count - 1);
         listBoxInvalidate(w, widthChanged);
    @@ -330,7 +336,7 @@ void wgtListBoxRemoveItem(WidgetT *w, int32_t idx) {
             return;
         }
     
    -    bool widthChanged = listItemsRemove(&d->items, idx);
    +    bool widthChanged = listItemsRemove(&d->items, wgtFont(w), idx);
     
         listSelRemove(&d->sel, oldCount, idx);
         listReorderCancel(w, &d->dragIdx, &d->dropIdx);
    @@ -351,7 +357,7 @@ void wgtListBoxSelectAll(WidgetT *w) {
     void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) {
         VALIDATE_WIDGET_VOID(w, sTypeId);
         ListBoxDataT *d            = (ListBoxDataT *)w->data;
    -    bool          widthChanged = listItemsSetExternal(&d->items, items, count);
    +    bool          widthChanged = listItemsSetExternal(&d->items, wgtFont(w), items, count);
     
         count = d->items.count;
     
    @@ -426,7 +432,7 @@ void wgtListBoxSetSelected(WidgetT *w, int32_t idx) {
     // listbox has a usable minimum height even when empty.
     void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
         ListBoxDataT *d        = (ListBoxDataT *)w->data;
    -    int32_t       maxItemW = d->items.maxLen * font->charWidth;
    +    int32_t       maxItemW = listItemsMaxWidth(&d->items, font);
         int32_t       minW     = LISTBOX_MIN_CHARS * font->charWidth;
     
         if (maxItemW < minW) {
    @@ -474,7 +480,7 @@ static void widgetListBoxOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int3
     
     
     // Key handling: multi-select Ctrl+A / Space go through the shared selection
    -// model; Enter activates; Left / Right scroll a character sideways; cursor
    +// model; Enter activates; Left / Right scroll a column sideways; cursor
     // movement (Up, Down, Home, End, PgUp, PgDn) uses widgetNavigateIndex and
     // printable keys type-ahead search.
     void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
    @@ -509,10 +515,9 @@ void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
             return;
         }
     
    -    AppContextT  *ctx = wgtGetContext(w);
    -    ListBoxGeomT  g;
    +    ListBoxGeomT g;
     
    -    listBoxGeom(w, &ctx->font, &g);
    +    listBoxGeom(w, wgtFont(w), &g);
     
         if (key == KEY_LEFT || key == KEY_RIGHT) {
             d->scrollCol = clampInt(d->scrollCol + (key == KEY_RIGHT ? 1 : -1), 0, g.maxScrollCol);
    @@ -551,7 +556,7 @@ void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
     void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
         ListBoxDataT      *d     = (ListBoxDataT *)hit->data;
         AppContextT       *ctx   = (AppContextT *)root->userData;
    -    const BitmapFontT *font  = &ctx->font;
    +    const BitmapFontT *font  = wgtFontFor(hit, &ctx->font);
         int32_t            count = d->items.count;
         int32_t            areaX = hit->x + LISTBOX_BORDER;
         int32_t            areaY = hit->y + LISTBOX_BORDER;
    @@ -574,7 +579,7 @@ void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
         }
     
         if (g.needH && vy >= areaY + g.innerH) {
    -        if (widgetScrollbarApplyHit(g.innerW, vx - areaX, d->items.maxLen, g.visibleCols, 1, g.visibleCols, &d->scrollCol, &dragOff)) {
    +        if (widgetScrollbarApplyHit(g.innerW, vx - areaX, g.totalCols, g.visibleCols, 1, g.visibleCols, &d->scrollCol, &dragOff)) {
                 sDragWidget     = hit;
                 d->sbDragOrient = ScrollbarHorizontalE;
                 d->sbDragOff    = dragOff;
    @@ -639,17 +644,19 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm
         lb->scrollCol = clampInt(lb->scrollCol, 0, g.maxScrollCol);
     
         // Clamp item text to the content width so long items don't overpaint
    -    // the scrollbar and neighboring widgets (no per-widget paint clip).
    -    // strnlen bounds the scan to the visible width instead of walking the
    -    // whole (possibly long) item string on every repaint.
    +    // the scrollbar and neighboring widgets (no per-widget paint clip):
    +    // characters scrolled even partly off the left are skipped and the rest
    +    // are cut to those that fit whole (listTextFit stops scanning there, not
    +    // at the end of a possibly long item, on every repaint).
         int32_t        areaX     = w->x + LISTBOX_BORDER;
         int32_t        innerX    = areaX + LISTBOX_PAD;
         int32_t        innerY    = w->y + LISTBOX_BORDER;
         int32_t        scrollPos = lb->scrollPos;
         int32_t        scrollCol = lb->scrollCol;
    +    int32_t        scrollX   = scrollCol * font->charWidth;
         bool           multi     = lb->sel.multi;
         const uint8_t *selBits   = lb->sel.bits;
    -    int32_t        maxChars  = DVX_MAX(g.innerW - LISTBOX_PAD, 0) / font->charWidth;
    +    int32_t        textW     = DVX_MAX(g.innerW - LISTBOX_PAD, 0);
     
         for (int32_t i = 0; i < g.visibleRows && (scrollPos + i) < count; i++) {
             int32_t     idx        = scrollPos + i;
    @@ -657,8 +664,8 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm
             uint32_t    ifg        = fg;
             uint32_t    ibg        = bg;
             const char *text       = lb->items.items[idx] ? lb->items.items[idx] : "";
    -        int32_t     len        = (int32_t)strnlen(text, (size_t)scrollCol + (size_t)maxChars);
    -        int32_t     first      = DVX_MIN(scrollCol, len);
    +        int32_t     first      = 0;
    +        int32_t     penX       = -scrollX;
             bool        isSelected = (multi && selBits) ? selBits[idx] != 0 : idx == lb->sel.cursor;
     
             if (isSelected) {
    @@ -667,7 +674,12 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm
                 rectFill(d, ops, areaX, iy, g.innerW, font->charHeight, ibg);
             }
     
    -        drawTextN(d, ops, font, innerX, iy, text + first, len - first, ifg, ibg, false);
    +        while (penX < 0 && text[first]) {
    +            penX += textWidthN(font, text + first, 1);
    +            first++;
    +        }
    +
    +        drawTextN(d, ops, font, innerX + penX, iy, text + first, listTextFit(font, text + first, textW - penX), ifg, ibg, false);
     
             // Draw cursor indicator in multi-select (dotted focus rect on cursor item)
             if (multi && idx == lb->sel.cursor && w == sFocusedWidget) {
    @@ -684,7 +696,7 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm
         }
     
         if (g.needH) {
    -        widgetDrawScrollbarH(d, ops, colors, areaX, innerY + g.innerH, g.innerW, lb->items.maxLen, g.visibleCols, scrollCol);
    +        widgetDrawScrollbarH(d, ops, colors, areaX, innerY + g.innerH, g.innerW, g.totalCols, g.visibleCols, scrollCol);
         }
     
         // The corner between the two bars
    @@ -724,22 +736,21 @@ static void widgetListBoxReorderDrop(WidgetT *w) {
     static void widgetListBoxReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
         (void)root;
         (void)x;
    -    ListBoxDataT *d   = (ListBoxDataT *)w->data;
    -    AppContextT  *ctx = wgtGetContext(w);
    -    ListBoxGeomT  g;
    +    ListBoxDataT      *d    = (ListBoxDataT *)w->data;
    +    const BitmapFontT *font = wgtFont(w);
    +    ListBoxGeomT       g;
     
    -    listBoxGeom(w, &ctx->font, &g);
    -    listReorderTrack(y - (w->y + LISTBOX_BORDER), ctx->font.charHeight, g.visibleRows, d->items.count, &d->scrollPos, &d->dropIdx);
    +    listBoxGeom(w, font, &g);
    +    listReorderTrack(y - (w->y + LISTBOX_BORDER), font->charHeight, g.visibleRows, d->items.count, &d->scrollPos, &d->dropIdx);
     }
     
     
     // Handle a scrollbar thumb drag on either bar.
     static void widgetListBoxScrollDragUpdate(WidgetT *w, ScrollbarOrientE orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) {
    -    ListBoxDataT *d   = (ListBoxDataT *)w->data;
    -    AppContextT  *ctx = wgtGetContext(w);
    +    ListBoxDataT *d = (ListBoxDataT *)w->data;
         ListBoxGeomT  g;
     
    -    listBoxGeom(w, &ctx->font, &g);
    +    listBoxGeom(w, wgtFont(w), &g);
     
         if (orient == ScrollbarVerticalE && g.maxScroll > 0) {
             int32_t relMouse = mouseY - (w->y + LISTBOX_BORDER) - WGT_SB_W - dragOff;
    @@ -748,7 +759,7 @@ static void widgetListBoxScrollDragUpdate(WidgetT *w, ScrollbarOrientE orient, i
         } else if (orient == ScrollbarHorizontalE && g.maxScrollCol > 0) {
             int32_t relMouse = mouseX - (w->x + LISTBOX_BORDER) - WGT_SB_W - dragOff;
     
    -        d->scrollCol = widgetScrollbarThumbDragScroll(g.innerW - WGT_SB_W * 2, d->items.maxLen, g.visibleCols, relMouse, g.maxScrollCol);
    +        d->scrollCol = widgetScrollbarThumbDragScroll(g.innerW - WGT_SB_W * 2, g.totalCols, g.visibleCols, relMouse, g.maxScrollCol);
         }
     }
     
    diff --git a/src/widgets/kpunch/listView/listView.dhs b/src/widgets/kpunch/listView/listView.dhs
    index ff529a2b..61561e34 100644
    --- a/src/widgets/kpunch/listView/listView.dhs
    +++ b/src/widgets/kpunch/listView/listView.dhs
    @@ -33,7 +33,7 @@
     
     .h2 ListView
     
    -A multi-column list with sortable headers. Supports single and multi-selection, column alignment, header click sorting, column resizing (drag a header border; double-click it to fit the contents), and drag-to-reorder. Data is provided as a flat array of strings (row-major order, one string per cell). wgtListViewSetData keeps a pointer to the caller's array; AddItem, RemoveRow, Clear, SetCell and drag-to-reorder work on a copy the widget makes first, so the caller's array is never modified.
    +A multi-column list with sortable headers. Supports single and multi-selection, column alignment, header click sorting, column resizing (drag a header border; double-click it to fit the contents), and drag-to-reorder. Data is provided as a flat array of strings (row-major order, one string per cell). wgtListViewSetData keeps a pointer to the caller's array; AddItem, RemoveRow, Clear, SetCell, and drag-to-reorder work on a copy the widget makes first, so the caller's array is never modified.
     
     Sorting and drag-to-reorder are exclusive: enabling reorder clears the sort, and while reorder is enabled wgtListViewSetSort and header clicks do not sort. A column width set by dragging or double-clicking a header border is kept when the data changes, until new columns are set.
     
    diff --git a/src/widgets/kpunch/listView/listview.bhs b/src/widgets/kpunch/listView/listview.bhs
    index 136dca4c..b08445c8 100644
    --- a/src/widgets/kpunch/listView/listview.bhs
    +++ b/src/widgets/kpunch/listView/listview.bhs
    @@ -62,7 +62,7 @@ A multi-column list with column headers. Supports sorting, multi-select, and dra
       SelectAll                                     Select all rows.
       SelectedRows$()                               Returns the numbers of the selected rows (as IsItemSelected numbers them) separated by spaces, "" when none: one call instead of IsItemSelected for every row of a long list.
       SetCell row%, col%, text$                     Set the text of a cell.
    -  SetColumns spec$                              Define columns. Pipe-delimited "Name,Width,Align|Name,Width": the width in characters (missing or 0 = as wide as the contents), then Left (the default), Center or Right for the column's text.
    +  SetColumns spec$                              Define columns. Pipe-delimited "Name,Width,Align|Name,Width": the width in characters (missing or 0 = as wide as the contents), then Left (the default), Center, or Right for the column's text.
       SetItemSelected index%, selected              Select or deselect a row.
       SetMultiSelect multi                          Enable or disable multi-select.
       SetReorderable reorderable                    Enable or disable row reordering.
    diff --git a/src/widgets/kpunch/listView/widgetListView.c b/src/widgets/kpunch/listView/widgetListView.c
    index 855dfb30..091a703f 100644
    --- a/src/widgets/kpunch/listView/widgetListView.c
    +++ b/src/widgets/kpunch/listView/widgetListView.c
    @@ -48,9 +48,11 @@
     // Column widths are tagged sizes (wgtPixels/wgtChars/wgtPercent, resolved
     // through wgtResolveSize) or 0 for auto (widest cell or title). They are
     // resolved lazily in resolveColumnWidths whenever totalColW is 0 (data
    -// changes reset it). A width the user set by dragging a header border or
    +// changes reset it) or the font is not the one they were resolved in
    +// (colWFont). A width the user set by dragging a header border or
     // double-clicking it (autosize) is kept in userColW and survives data
    -// changes until new columns are set.
    +// changes until new columns are set. Titles and cells are cut to the
    +// characters that fit whole in their column, in any font.
     //
     // Column resize: dragging a column header border (3px hot zone) resizes the
     // column. The drag state (resizeCol, resizeStartX, resizeOrigW,
    @@ -118,6 +120,7 @@ typedef struct {
         char              **ownedCells;     // stb_ds array of strdup'd strings
         ListViewColT       *ownedColDefs;   // stb_ds array (BASIC SetColumns)
         char              **ownedColTitles; // stb_ds array of strdup'd titles
    +    FontStampT          colWFont;       // font resolvedColW was resolved in
         void              (*onHeaderClick)(struct WidgetT *w, int32_t col, ListViewSortE dir);
         ListSelT            sel;
         int32_t             colCount;
    @@ -411,22 +414,22 @@ static void listViewAdoptCells(WidgetT *w) {
     
     // Width that fits the column's title and its widest cell.
     static int32_t listViewAutoColWidth(const ListViewDataT *lv, int32_t col, const BitmapFontT *font) {
    -    const char *title  = lv->cols[col].title;
    -    int32_t     maxLen = title ? (int32_t)strlen(title) : 0;
    +    const char *title = lv->cols[col].title;
    +    int32_t     maxW  = title ? textWidth(font, title) : 0;
     
         for (int32_t r = 0; r < lv->rowCount; r++) {
             const char *cell = lv->cellData[r * lv->colCount + col];
     
             if (cell) {
    -            int32_t slen = (int32_t)strlen(cell);
    +            int32_t width = textWidth(font, cell);
     
    -            if (slen > maxLen) {
    -                maxLen = slen;
    +            if (width > maxW) {
    +                maxW = width;
                 }
             }
         }
     
    -    return maxLen * font->charWidth + LISTVIEW_COL_PAD;
    +    return maxW + LISTVIEW_COL_PAD;
     }
     
     
    @@ -552,11 +555,12 @@ static int32_t listViewCompareRows(const ListViewDataT *lv, int32_t a, int32_t b
     }
     
     
    -// Column widths are resolved lazily: data changes only reset totalColW.
    +// Column widths are resolved lazily: data changes only reset totalColW,
    +// and widths resolved in another font are resolved again.
     static void listViewEnsureColWidths(const WidgetT *w, const BitmapFontT *font) {
         const ListViewDataT *lv = (const ListViewDataT *)w->data;
     
    -    if (lv->totalColW == 0 && lv->colCount > 0) {
    +    if ((lv->totalColW == 0 || !fontStampIs(&lv->colWFont, font)) && lv->colCount > 0) {
             resolveColumnWidths(w, font);
         }
     }
    @@ -701,6 +705,7 @@ static void resolveColumnWidths(const WidgetT *w, const BitmapFontT *font) {
         }
     
         lv->totalColW = totalW;
    +    fontStampSet(&lv->colWFont, font);
     }
     
     
    @@ -1066,10 +1071,11 @@ void wgtListViewSetSelected(WidgetT *w, int32_t idx) {
         listSelSetCursor(&lv->sel, lv->rowCount, idx);
     
         if (ctx && lv->sel.cursor >= 0) {
    -        ListViewGeomT g;
    +        const BitmapFontT *font = wgtFontFor(w, &ctx->font);
    +        ListViewGeomT      g;
     
    -        listViewEnsureColWidths(w, &ctx->font);
    -        listViewCalcGeom(w, lv, &ctx->font, &g);
    +        listViewEnsureColWidths(w, font);
    +        listViewCalcGeom(w, lv, font, &g);
             listViewKeepVisible(lv, listDisplayRowOf(listViewOrder(lv), lv->rowCount, lv->sel.cursor), g.visibleRows);
         }
     
    @@ -1118,8 +1124,7 @@ bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy) {
             return false;
         }
     
    -    AppContextT       *ctx       = wgtGetContext(w);
    -    const BitmapFontT *font      = &ctx->font;
    +    const BitmapFontT *font      = wgtFont(w);
         int32_t            headerH   = font->charHeight + LISTVIEW_HEADER_VPAD;
         int32_t            headerTop = w->y + LISTVIEW_BORDER;
     
    @@ -1240,13 +1245,13 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
             return;
         }
     
    -    AppContextT   *ctx        = wgtGetContext(w);
    -    const int32_t *order      = listViewOrder(lv);
    -    int32_t        displaySel = listDisplayRowOf(order, rowCount, lv->sel.cursor);
    -    ListViewGeomT  g;
    +    const BitmapFontT *font       = wgtFont(w);
    +    const int32_t     *order      = listViewOrder(lv);
    +    int32_t            displaySel = listDisplayRowOf(order, rowCount, lv->sel.cursor);
    +    ListViewGeomT      g;
     
    -    listViewEnsureColWidths(w, &ctx->font);
    -    listViewCalcGeom(w, lv, &ctx->font, &g);
    +    listViewEnsureColWidths(w, font);
    +    listViewCalcGeom(w, lv, font, &g);
     
         int32_t newDisplaySel = widgetNavigateIndex(key, displaySel, rowCount, g.visibleRows);
     
    @@ -1282,7 +1287,7 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
         sFocusedWidget = hit;
         ListViewDataT     *lv   = (ListViewDataT *)hit->data;
         AppContextT       *ctx  = (AppContextT *)root->userData;
    -    const BitmapFontT *font = &ctx->font;
    +    const BitmapFontT *font = wgtFontFor(hit, &ctx->font);
         ListViewGeomT      g;
     
         listViewEnsureColWidths(hit, font);
    @@ -1506,7 +1511,7 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
                 }
     
                 if (title && availTextW > 0) {
    -                int32_t titleLen = (int32_t)strnlen(title, (size_t)(availTextW / font->charWidth));
    +                int32_t titleLen = listTextFit(font, title, availTextW);
     
                     if (titleLen > 0) {
                         drawTextN(d, ops, font, textX, textY, title, titleLen, colors->contentFg, colors->windowFace, true);
    @@ -1568,9 +1573,9 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
                     const char *cell   = lv->cellData[dataRow * colCount + c];
     
                     if (cell && availW > 0) {
    -                    // strnlen bounds the scan to what fits in the column.
    -                    int32_t cellLen   = (int32_t)strnlen(cell, (size_t)(availW / font->charWidth));
    -                    int32_t renderedW = cellLen * font->charWidth;
    +                    // listTextFit bounds the scan to what fits in the column.
    +                    int32_t cellLen   = listTextFit(font, cell, availW);
    +                    int32_t renderedW = textWidthN(font, cell, cellLen);
                         int32_t tx        = cellX + LISTVIEW_PAD;
     
                         if (lv->cols[c].align == ListViewAlignRightE) {
    @@ -1651,12 +1656,12 @@ static void widgetListViewReorderDrop(WidgetT *w) {
     static void widgetListViewReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
         (void)root;
         (void)x;
    -    ListViewDataT *lv  = (ListViewDataT *)w->data;
    -    AppContextT   *ctx = wgtGetContext(w);
    -    ListViewGeomT  g;
    +    ListViewDataT     *lv   = (ListViewDataT *)w->data;
    +    const BitmapFontT *font = wgtFont(w);
    +    ListViewGeomT      g;
     
    -    listViewCalcGeom(w, lv, &ctx->font, &g);
    -    listReorderTrack(y - (w->y + LISTVIEW_BORDER + g.headerH), ctx->font.charHeight, g.visibleRows, lv->rowCount, &lv->scrollPos, &lv->dropIdx);
    +    listViewCalcGeom(w, lv, font, &g);
    +    listReorderTrack(y - (w->y + LISTVIEW_BORDER + g.headerH), font->charHeight, g.visibleRows, lv->rowCount, &lv->scrollPos, &lv->dropIdx);
     }
     
     
    @@ -1688,10 +1693,9 @@ static void widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t d
             return;
         }
     
    -    AppContextT  *ctx = wgtGetContext(w);
         ListViewGeomT g;
     
    -    listViewCalcGeom(w, lv, &ctx->font, &g);
    +    listViewCalcGeom(w, lv, wgtFont(w), &g);
     
         if (orient == LISTVIEW_DRAG_VSB) {
             if (g.maxScrollV <= 0) {
    diff --git a/src/widgets/kpunch/richEdit/richEdit.h b/src/widgets/kpunch/richEdit/richEdit.h
    new file mode 100644
    index 00000000..4c27a624
    --- /dev/null
    +++ b/src/widgets/kpunch/richEdit/richEdit.h
    @@ -0,0 +1,122 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// richEdit.h  -- RichEdit widget API
    +//
    +// A word processor's text box: text in fonts, sizes, styles, and colors
    +// of its own, in paragraphs aligned left, centered, or right, edited with
    +// the keyboard and mouse.  Its document is styled text (richText.h);
    +// TextRTF reads and writes it as RTF.  Offsets count characters from 0,
    +// one between two paragraphs.  Style and alignment calls change the
    +// selection, or with none, the text typed next (alignment: the caret's
    +// paragraph); the Get calls tell the selection's first character.
    +#ifndef RICHEDIT_H
    +#define RICHEDIT_H
    +
    +#include "dvxWgt.h"
    +
    +// SetSelColor's "the control's ForeColor" (richText.h's RICH_COLOR_DEFAULT)
    +#define RICHEDIT_COLOR_DEFAULT 0xFFFFFFFFu
    +
    +// Fired (wgtFireEvent, no arguments) when the selection or caret moves
    +#define RICHEDIT_EVT_SELCHANGE "SelChange"
    +
    +typedef struct {
    +    WgtApiHeaderT header;
    +    WidgetT    *(*create)(WidgetT *parent);
    +    const char *(*getRtf)(const WidgetT *w);
    +    void        (*setRtf)(WidgetT *w, const char *rtf);
    +    const char *(*getSelRtf)(const WidgetT *w);
    +    void        (*setSelRtf)(WidgetT *w, const char *rtf);
    +    int32_t     (*getSelStart)(const WidgetT *w);
    +    void        (*setSelStart)(WidgetT *w, int32_t start);
    +    int32_t     (*getSelLength)(const WidgetT *w);
    +    void        (*setSelLength)(WidgetT *w, int32_t len);
    +    const char *(*getSelText)(const WidgetT *w);
    +    void        (*setSelText)(WidgetT *w, const char *text);
    +    const char *(*getSelFontName)(const WidgetT *w);
    +    int32_t     (*getSelFontSize)(const WidgetT *w);
    +    void        (*setSelFont)(WidgetT *w, const char *family, int32_t size);
    +    uint32_t    (*getSelStyle)(const WidgetT *w);
    +    void        (*setSelStyle)(WidgetT *w, uint32_t style, uint32_t mask);
    +    uint32_t    (*getSelColor)(const WidgetT *w);
    +    void        (*setSelColor)(WidgetT *w, uint32_t rgb);
    +    int32_t     (*getSelAlign)(const WidgetT *w);
    +    void        (*setSelAlign)(WidgetT *w, int32_t align);
    +    bool        (*canUndo)(const WidgetT *w);
    +    bool        (*canRedo)(const WidgetT *w);
    +    void        (*undo)(WidgetT *w);
    +    void        (*redo)(WidgetT *w);
    +    void        (*cut)(WidgetT *w);
    +    void        (*copy)(WidgetT *w);
    +    void        (*paste)(WidgetT *w);
    +    void        (*selectAll)(WidgetT *w);
    +    int32_t     (*find)(WidgetT *w, const char *text, bool matchCase);
    +    int32_t     (*loadFile)(WidgetT *w, const char *path);
    +    int32_t     (*saveFile)(WidgetT *w, const char *path);
    +} RichEditApiT;
    +
    +// Consumer side: the accessor and convenience macros.  The widget's own
    +// implementation defines RICHEDIT_IMPL so its functions keep their names.
    +#ifndef RICHEDIT_IMPL
    +
    +static inline const RichEditApiT *dvxRichEditApi(void) {
    +    static const RichEditApiT *sApi;
    +    if (!sApi) { sApi = (const RichEditApiT *)wgtGetApi("richedit", sizeof(RichEditApiT)); }
    +    return sApi;
    +}
    +
    +#define wgtRichEdit(parent)                     dvxRichEditApi()->create(parent)
    +#define wgtRichEditGetRtf(w)                    dvxRichEditApi()->getRtf(w)
    +#define wgtRichEditSetRtf(w, rtf)               dvxRichEditApi()->setRtf(w, rtf)
    +#define wgtRichEditGetSelRtf(w)                 dvxRichEditApi()->getSelRtf(w)
    +#define wgtRichEditSetSelRtf(w, rtf)            dvxRichEditApi()->setSelRtf(w, rtf)
    +#define wgtRichEditGetSelStart(w)               dvxRichEditApi()->getSelStart(w)
    +#define wgtRichEditSetSelStart(w, start)        dvxRichEditApi()->setSelStart(w, start)
    +#define wgtRichEditGetSelLength(w)              dvxRichEditApi()->getSelLength(w)
    +#define wgtRichEditSetSelLength(w, len)         dvxRichEditApi()->setSelLength(w, len)
    +#define wgtRichEditGetSelText(w)                dvxRichEditApi()->getSelText(w)
    +#define wgtRichEditSetSelText(w, text)          dvxRichEditApi()->setSelText(w, text)
    +#define wgtRichEditGetSelFontName(w)            dvxRichEditApi()->getSelFontName(w)
    +#define wgtRichEditGetSelFontSize(w)            dvxRichEditApi()->getSelFontSize(w)
    +#define wgtRichEditSetSelFont(w, family, size)  dvxRichEditApi()->setSelFont(w, family, size)
    +#define wgtRichEditGetSelStyle(w)               dvxRichEditApi()->getSelStyle(w)
    +#define wgtRichEditSetSelStyle(w, style, mask)  dvxRichEditApi()->setSelStyle(w, style, mask)
    +#define wgtRichEditGetSelColor(w)               dvxRichEditApi()->getSelColor(w)
    +#define wgtRichEditSetSelColor(w, rgb)          dvxRichEditApi()->setSelColor(w, rgb)
    +#define wgtRichEditGetSelAlign(w)               dvxRichEditApi()->getSelAlign(w)
    +#define wgtRichEditSetSelAlign(w, align)        dvxRichEditApi()->setSelAlign(w, align)
    +#define wgtRichEditCanUndo(w)                   dvxRichEditApi()->canUndo(w)
    +#define wgtRichEditCanRedo(w)                   dvxRichEditApi()->canRedo(w)
    +#define wgtRichEditUndo(w)                      dvxRichEditApi()->undo(w)
    +#define wgtRichEditRedo(w)                      dvxRichEditApi()->redo(w)
    +#define wgtRichEditCut(w)                       dvxRichEditApi()->cut(w)
    +#define wgtRichEditCopy(w)                      dvxRichEditApi()->copy(w)
    +#define wgtRichEditPaste(w)                     dvxRichEditApi()->paste(w)
    +#define wgtRichEditSelectAll(w)                 dvxRichEditApi()->selectAll(w)
    +#define wgtRichEditFind(w, text, matchCase)     dvxRichEditApi()->find(w, text, matchCase)
    +#define wgtRichEditLoadFile(w, path)            dvxRichEditApi()->loadFile(w, path)
    +#define wgtRichEditSaveFile(w, path)            dvxRichEditApi()->saveFile(w, path)
    +
    +#endif // RICHEDIT_IMPL
    +
    +#endif // RICHEDIT_H
    diff --git a/src/widgets/kpunch/richEdit/richedit.bhs b/src/widgets/kpunch/richEdit/richedit.bhs
    new file mode 100644
    index 00000000..a5ed2386
    --- /dev/null
    +++ b/src/widgets/kpunch/richEdit/richedit.bhs
    @@ -0,0 +1,134 @@
    +# The MIT License (MIT)
    +#
    +# Copyright (C) 2026 Scott Duensing
    +#
    +# Permission is hereby granted, free of charge, to any person obtaining a copy
    +# of this software and associated documentation files (the "Software"), to
    +# deal in the Software without restriction, including without limitation the
    +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +# sell copies of the Software, and to permit persons to whom the Software is
    +# furnished to do so, subject to the following conditions:
    +#
    +# The above copyright notice and this permission notice shall be included in
    +# all copies or substantial portions of the Software.
    +#
    +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +# IN THE SOFTWARE.
    +
    +.topic ctrl.richedit
    +.title RichEdit
    +.toc 1 RichEdit
    +.index RichEdit
    +.index TextRTF
    +.index SelRTF
    +.index SelBold
    +.index SelItalic
    +.index SelUnderline
    +.index SelStrikethru
    +.index SelColor
    +.index SelFontName
    +.index SelFontSize
    +.index SelAlignment
    +.index LoadFile
    +.index SaveFile
    +.index RTF
    +
    +.h1 RichEdit
    +
    +VB Equivalent: RichTextBox -- DVX Widget: richedit
    +
    +A text box for text in many fonts, sizes, and colors, bold, italic, underlined, or struck through, in paragraphs aligned left, centered, or right -- a small word processor's page. The user types, selects, and edits as in any text box; a program reads and changes the selection's look with the Sel properties, and the whole text, with its looks, as RTF (Rich Text Format, the format word processors save), so it can be saved to a file and loaded again.
    +
    +Text starts in the control's own font (FontName, FontSize, and the other Font properties, or the form's) and in its ForeColor; text left in them follows the control when they change. The Sel properties read the first character of the selection; setting one changes the selection, or with nothing selected, the text typed next. SelStart counts characters from 0, and the break between two paragraphs is one character.
    +
    +.h2 Type-Specific Properties
    +
    +.table
    +  Property        Type      Description
    +  --------        ----      -----------
    +  TextRTF         String    The whole text with its fonts, styles, colors, and alignment, as RTF. Setting it replaces the text (plain text is taken as it is).
    +  SelRTF          String    The selection as RTF. Setting it replaces the selection.
    +  SelStart        Integer   Where the selection starts (the caret when nothing is selected).
    +  SelLength       Integer   How many characters are selected.
    +  SelText         String    The selection's text. Setting it replaces the selection with plain text.
    +  SelBold         Boolean   Bold.
    +  SelItalic       Boolean   Italic.
    +  SelUnderline    Boolean   Underlined.
    +  SelStrikethru   Boolean   Struck through.
    +  SelColor        Long      The color, as RGB() makes it; -1 for the control's ForeColor.
    +  SelFontName     String    The font family ("" for the control's own).
    +  SelFontSize     Integer   The size in pixels (the nearest one the family comes in).
    +  SelAlignment    Integer   The alignment of the selection's paragraphs: 0 left, 1 centered, 2 right.
    +  CanUndo         Boolean   An edit can be undone (read-only).
    +  CanRedo         Boolean   An undone edit can be done again (read-only).
    +.endtable
    +
    +Text (a common property) is the plain text, a line per paragraph; setting it replaces everything with plain text. ReadOnly lets the user select and copy but not change the text.
    +
    +.h2 Type-Specific Methods
    +
    +.table
    +  Method                 Description
    +  ------                 -----------
    +  Undo                   Undo the last edit (typing a run of characters is one edit). Up to 100 edits are kept.
    +  Redo                   Do an undone edit again.
    +  Cut, Copy, Paste       The clipboard. Text copied from a RichEdit pastes back into a RichEdit with its looks; text from elsewhere takes the look of the text typed next.
    +  SelectAll              Select all the text.
    +  Find(text)             Select the next place text is (any case), from the selection on and around past the end; returns where it starts, or -1.
    +  LoadFile(path)         Load a file: RTF when it is RTF, else plain text. Returns 0, or -1 when it cannot be read.
    +  SaveFile(path)         Save the text: as RTF when path ends in .rtf, else as plain text. Returns 0, or -1 when it cannot be written.
    +.endtable
    +
    +.h2 Type-Specific Events
    +
    +.table
    +  Event        Parameters   Description
    +  -----        ----------   -----------
    +  SelChange    (none)       The selection or the caret moved.
    +.endtable
    +
    +Change fires after every edit. Default Event: Change
    +
    +.h2 Keys
    +
    +.table
    +  Key                         Action
    +  ---                         ------
    +  Ctrl+B, Ctrl+I, Ctrl+U      Bold, italic, or underline on or off.
    +  Ctrl+L, Ctrl+E, Ctrl+R      Align the paragraph left, centered, or right.
    +  Ctrl+Z, Ctrl+Y              Undo, Redo.
    +  Ctrl+X, Ctrl+C, Ctrl+V      Cut, Copy, Paste.
    +  Ctrl+A                      Select all.
    +  Ctrl+Left, Ctrl+Right       A word back or on.
    +  Ctrl+Home, Ctrl+End         The start or the end of the text.
    +  Shift with a moving key     Extend the selection.
    +.endtable
    +
    +A double click selects a word, a triple click the paragraph.
    +
    +.h2 Example
    +
    +.code
    +Sub Form_Load ()
    +    RichEdit1.Text = "Dear friend,"
    +    RichEdit1.SelStart = 0
    +    RichEdit1.SelLength = 4
    +    RichEdit1.SelBold = True
    +    RichEdit1.SelColor = RGB(0, 0, 160)
    +End Sub
    +
    +Sub cmdItalic_Click ()
    +    RichEdit1.SelItalic = Not RichEdit1.SelItalic
    +End Sub
    +
    +Sub cmdSave_Click ()
    +    If RichEdit1.SaveFile("LETTER.RTF") <> 0 Then MsgBox "The letter could not be saved."
    +End Sub
    +.endcode
    +
    +.link ctrl.common.props Common Properties, Events, and Methods
    diff --git a/src/widgets/kpunch/richEdit/richedit.bmp b/src/widgets/kpunch/richEdit/richedit.bmp
    new file mode 100644
    index 00000000..1894a973
    --- /dev/null
    +++ b/src/widgets/kpunch/richEdit/richedit.bmp
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:6a76a5e9c931cedc2849b7b73f6eda562289c15d24a3ea3130e7cf24d204ded3
    +size 1782
    diff --git a/src/widgets/kpunch/richEdit/richedit.dep b/src/widgets/kpunch/richEdit/richedit.dep
    new file mode 100644
    index 00000000..87c1d697
    --- /dev/null
    +++ b/src/widgets/kpunch/richEdit/richedit.dep
    @@ -0,0 +1,2 @@
    +texthelp
    +richtext
    diff --git a/src/widgets/kpunch/richEdit/richedit.dhs b/src/widgets/kpunch/richEdit/richedit.dhs
    new file mode 100644
    index 00000000..bda53b44
    --- /dev/null
    +++ b/src/widgets/kpunch/richEdit/richedit.dhs
    @@ -0,0 +1,95 @@
    +# The MIT License (MIT)
    +#
    +# Copyright (C) 2026 Scott Duensing
    +#
    +# Permission is hereby granted, free of charge, to any person obtaining a copy
    +# of this software and associated documentation files (the "Software"), to
    +# deal in the Software without restriction, including without limitation the
    +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +# sell copies of the Software, and to permit persons to whom the Software is
    +# furnished to do so, subject to the following conditions:
    +#
    +# The above copyright notice and this permission notice shall be included in
    +# all copies or substantial portions of the Software.
    +#
    +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +# IN THE SOFTWARE.
    +
    +.section Technical Reference (for Developers)|Widgets
    +.topic widget.richedit
    +.title RichEdit
    +.toc 0 RichEdit
    +.index RichEdit
    +.index wgtRichEdit
    +.index wgtRichEditGetRtf
    +.index wgtRichEditSetSelStyle
    +
    +.h2 RichEdit
    +
    +A text box for styled text: runs in fonts, sizes, styles, and colors, in paragraphs aligned left, centered, or right, edited with the keyboard and the mouse, with undo and redo, a clipboard that keeps styles between RichEdit controls, find, and RTF in and out. The document is a RichTextT of the richtext library (richtext/richText.h); a style's family "" and size 0 are the widget's own font (wgtSetFont, or its container's) and RICHEDIT_COLOR_DEFAULT its fgColor, so such text follows the widget.
    +
    +Each paragraph is laid out by a text flow of its own (dvxFlow.h): an edit lays out only the paragraphs it touched, and the paragraphs' tops are kept for binary searches from an offset or a row. The widget polls (WCLASS_NEEDS_POLL) and has a quick repaint (WGT_METHOD_QUICK_REPAINT): an edit that left every paragraph's height alone, a caret or selection move, and the caret's blink repaint only the rows they touched, painted straight into the window's content buffer (widgetQuickPaintBegin); a change of height, a scroll, or a new width repaints the widget. Undo keeps for each edit what it replaced and the length it put in its place (alignment changes keep the old alignments); typed characters join one step; 100 steps are kept.
    +
    +Depends on the "texthelp" (word boundaries, the caret blink time, scroll bar drags) and "richtext" libraries (declared in richedit.dep).
    +
    +Header: widgets/richEdit.h
    +
    +.h3 Creation
    +
    +.code
    +WidgetT *re = wgtRichEdit(parent);
    +
    +wgtSetText(re, "Dear friend,");
    +wgtRichEditSetSelStart(re, 0);
    +wgtRichEditSetSelLength(re, 4);
    +wgtRichEditSetSelStyle(re, FONT_STYLE_BOLD, FONT_STYLE_BOLD);
    +wgtRichEditSetSelColor(re, DVX_RGB(0, 0, 160));
    +.endcode
    +
    +.h3 API Functions
    +
    +.table
    +  Function                                              Description
    +  --------                                              -----------
    +  WidgetT *wgtRichEdit(parent)                          Create an empty rich edit box.
    +  const char *wgtRichEditGetRtf(w)                      The whole text as RTF (valid until the next call that returns RTF).
    +  void wgtRichEditSetRtf(w, rtf)                        Replace the whole text from RTF (plain text when it is not RTF); undo is emptied.
    +  const char *wgtRichEditGetSelRtf(w)                   The selection as RTF.
    +  void wgtRichEditSetSelRtf(w, rtf)                     Replace the selection from RTF.
    +  int32_t wgtRichEditGetSelStart(w) / SetSelStart       Where the selection starts; setting it places the caret.
    +  int32_t wgtRichEditGetSelLength(w) / SetSelLength     The selection's length; setting it selects from SelStart.
    +  const char *wgtRichEditGetSelText(w) / SetSelText     The selection's text; setting it replaces the selection with plain text.
    +  const char *wgtRichEditGetSelFontName(w)              The selection's family (the widget's own when it uses that).
    +  int32_t wgtRichEditGetSelFontSize(w)                  The selection's pixel size (as opened: the nearest the family has).
    +  void wgtRichEditSetSelFont(w, family, size)           The family (NULL: unchanged; "": the widget's) and size (0: unchanged).
    +  uint32_t wgtRichEditGetSelStyle(w)                    The selection's FONT_STYLE_* bits.
    +  void wgtRichEditSetSelStyle(w, style, mask)           Make the bits in mask style's.
    +  uint32_t wgtRichEditGetSelColor(w) / SetSelColor      0x00RRGGBB or RICHEDIT_COLOR_DEFAULT.
    +  int32_t wgtRichEditGetSelAlign(w) / SetSelAlign       FLOW_ALIGN_* of the selection's paragraphs.
    +  bool wgtRichEditCanUndo(w) / CanRedo                  Whether there is a step to undo or redo.
    +  void wgtRichEditUndo(w) / Redo                        Undo or redo a step.
    +  void wgtRichEditCut(w) / Copy / Paste                 The clipboard (plain text there; the styled copy kept for a paste of the same text).
    +  void wgtRichEditSelectAll(w)                          Select everything.
    +  int32_t wgtRichEditFind(w, text, matchCase)           Select the next match from the selection's end, around past the end; its offset or -1.
    +  int32_t wgtRichEditLoadFile(w, path)                  Load RTF or plain text; 0 or -1.
    +  int32_t wgtRichEditSaveFile(w, path)                  Save RTF (path ending in .rtf) or plain text; 0 or -1.
    +.endtable
    +
    +The style and alignment setters change the selection, or with none, the style typing uses next (alignment: the caret's paragraph); each change is an undo step and fires Change. wgtGetText / wgtSetText are the plain text, a line per paragraph. RICHEDIT_EVT_SELCHANGE ("SelChange") is fired with wgtFireEvent when the selection or the caret moves.
    +
    +.h3 API Struct (wgtRegisterApi "richedit")
    +
    +The slots are the functions above in order: create, getRtf, setRtf, getSelRtf, setSelRtf, getSelStart, setSelStart, getSelLength, setSelLength, getSelText, setSelText, getSelFontName, getSelFontSize, setSelFont, getSelStyle, setSelStyle, getSelColor, setSelColor, getSelAlign, setSelAlign, canUndo, canRedo, undo, redo, cut, copy, paste, selectAll, find, loadFile, and saveFile.
    +
    +.h3 BASIC Interface
    +
    +Properties: TextRTF and SelRTF (wgtRichEditGetRtf / SetRtf, GetSelRtf / SetSelRtf), SelStart, SelLength, SelText, SelBold, SelItalic, SelUnderline, SelStrikethru (one FONT_STYLE_* bit of SetSelStyle each), SelColor (a BASIC color; -1 for ForeColor), SelFontName and SelFontSize (wgtRichEditSetSelFont with the other part left alone), SelAlignment, and the read-only CanUndo and CanRedo. Methods: Undo, Redo, Cut, Copy, Paste, SelectAll, Find (any case), LoadFile, and SaveFile. Event: SelChange; default event Change.
    +
    +.h3 Methods Used
    +
    +PAINT, CALC_MIN_SIZE (20 by 4 characters of its font, with border and scroll bar), ON_MOUSE, ON_KEY, ON_DRAG_UPDATE, ON_DRAG_END, ON_WHEEL, ON_FOCUS, ON_BLUR, POLL, QUICK_REPAINT, GET_TEXT, SET_TEXT, CLEAR_SELECTION, DESTROY. Flags: WCLASS_FOCUSABLE, WCLASS_NEEDS_POLL.
    diff --git a/src/widgets/kpunch/richEdit/richedit.res b/src/widgets/kpunch/richEdit/richedit.res
    new file mode 100644
    index 00000000..ea6d7515
    --- /dev/null
    +++ b/src/widgets/kpunch/richEdit/richedit.res
    @@ -0,0 +1,7 @@
    +icon24      icon  richedit.bmp
    +name        text  "RichEdit"
    +author      text  "Scott Duensing"
    +copyright   text  "Copyright 2026 Scott Duensing"
    +publisher   text  "Kangaroo Punch Studios"
    +description text  "A text box for text in many fonts, styles, and colors"
    +version     text  "1.0"
    diff --git a/src/widgets/kpunch/richEdit/widgetRichEdit.c b/src/widgets/kpunch/richEdit/widgetRichEdit.c
    new file mode 100644
    index 00000000..dda19cec
    --- /dev/null
    +++ b/src/widgets/kpunch/richEdit/widgetRichEdit.c
    @@ -0,0 +1,1994 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// widgetRichEdit.c  -- RichEdit widget: a text box for styled text
    +//
    +// The document is a RichTextT (the richtext library).  Each paragraph is
    +// laid out by a text flow of its own (dvxFlow.h), so an edit lays out
    +// only the paragraphs it touched again; the paragraphs' tops are kept, so
    +// a click or the caret finds its paragraph by a binary search.  Painting
    +// comes in two kinds: the whole control (paint), and a band of rows (the
    +// quick repaint, through the poll loop) for an edit that changed no
    +// paragraph's height, a selection or caret move, and the caret's blink --
    +// typing repaints its line, not the control.
    +//
    +// Undo keeps, for each change, what it replaced (a piece of styled text)
    +// and the length it put in its place; undoing swaps them back, and redo
    +// the same.  Typed characters join one step.  Cut and Copy put plain text
    +// on the clipboard and keep the styled piece here: pasting that same text
    +// brings its styles back.
    +
    +#define RICHEDIT_IMPL
    +#include "dvxWgtP.h"
    +#include "dvxApp.h"
    +#include "dvxFlow.h"
    +#include "dvxPlat.h"
    +#include "dvxText.h"
    +#include "richEdit.h"
    +#include "../../../libs/kpunch/richtext/richText.h"
    +#include "../../../libs/kpunch/texthelp/textHelp.h"
    +#include "stb_ds_wrap.h"
    +
    +#include 
    +#include 
    +#include 
    +#include 
    +
    +// ABI stamp checked by the loader (see DVX_ABI_VERSION).
    +DVX_ABI_STAMP(richedit);
    +
    +#define RICH_BORDER       2          // sunken bevel
    +#define RICH_PAD          2          // between the bevel and the text
    +#define RICH_CARET_W      1
    +#define RICH_MIN_COLS     20         // the smallest size, in the control's font
    +#define RICH_MIN_ROWS     4
    +#define RICH_UNDO_MAX     100        // undo steps kept
    +#define RICH_WHEEL_LINES  3          // lines a wheel notch scrolls
    +#define RICH_NONE         (-1)
    +#define RICH_EXT_RTF      ".rtf"
    +#define RICH_CHAR_NEWLINE '\n'
    +#define BAS_COLOR_DEFAULT (-1)       // SetColor in BASIC: the control's ForeColor
    +
    +// Ctrl+letter keys the box takes (KEY_CTRL_A ... are in dvxTypes.h)
    +#define RICH_KEY_BOLD      0x02      // Ctrl+B
    +#define RICH_KEY_CENTER    0x05      // Ctrl+E
    +#define RICH_KEY_ITALIC    0x09      // Ctrl+I (with Ctrl held: Tab otherwise)
    +#define RICH_KEY_LEFT      0x0C      // Ctrl+L
    +#define RICH_KEY_RIGHT     0x12      // Ctrl+R
    +#define RICH_KEY_UNDERLINE 0x15      // Ctrl+U
    +#define RICH_KEY_REDO      0x19      // Ctrl+Y
    +
    +static int32_t sTypeId = -1;
    +
    +// One paragraph of the document, laid out
    +typedef struct {
    +    int32_t    start;                // its first offset
    +    int32_t    len;                  // its characters, its '\n' not counted
    +    TextFlowT *flow;                 // NULL: to lay out again
    +    int32_t    y;                    // its top in the document
    +    int32_t    height;
    +} RichParaT;
    +
    +// One undo (or redo) step: putting was back over at to at + len undoes it
    +typedef struct {
    +    int32_t   at;
    +    int32_t   len;
    +    RichTextT was;
    +    int32_t  *aligns;                // stb_ds: an alignment step (was unused): the paragraphs' alignments from para
    +    int32_t   para;
    +    int32_t   anchor;                // the selection before the step
    +    int32_t   caret;
    +    bool      typing;                // typed characters: the next one joins it
    +} RichUndoT;
    +
    +typedef struct {
    +    RichTextT     doc;
    +    RichParaT    *paras;             // stb_ds
    +    BitmapFontT **fonts;             // stb_ds, per doc style; NULL: not opened yet
    +    FontStampT    ownFont;           // the control's font the style fonts were opened for
    +    int32_t       layoutW;           // the width the paragraphs were laid out at
    +    int32_t       docH;
    +    int32_t       anchor;            // the selection runs from anchor to caret
    +    int32_t       caret;
    +    int32_t       wantX;             // Up / Down keep this x (RICH_NONE: the caret's)
    +    int32_t       typingStyle;       // the style typing uses (RICH_NONE: the text's at the caret)
    +    int32_t       scrollY;
    +    RichUndoT    *undo;              // stb_ds
    +    RichUndoT    *redo;              // stb_ds
    +    char         *textOut;           // stb_ds: the last text handed out
    +    char         *rtfOut;            // malloc'd: the last RTF handed out
    +    int32_t       dirtyTop;          // the document rows to quick-repaint (top >= bottom: none)
    +    int32_t       dirtyBottom;
    +    bool          fullPaint;         // the whole control is to paint
    +    bool          caretOn;           // the caret's blink phase
    +    PlatformTicksT blinkAt;
    +    bool          sbDragging;
    +    int32_t       sbDragOff;
    +    bool          selecting;         // a mouse drag selects
    +} RichEditDataT;
    +
    +// Where things are, for paint, mouse and keys alike
    +typedef struct {
    +    int32_t textX;                   // the text's left edge
    +    int32_t textY;                   // the document's top on screen (scrolled)
    +    int32_t textW;                   // the width paragraphs wrap at
    +    int32_t viewY;                   // the text area: its top and height
    +    int32_t viewH;
    +    int32_t textH;                   // the rows of text it shows (its height less the padding)
    +    int32_t sbX;                     // the scroll bar
    +    int32_t sbY;
    +    int32_t sbH;
    +} RichGeomT;
    +
    +// What Copy and Cut kept: pasting clipText brings clip back
    +static RichTextT sClip;
    +static char     *sClipText  = NULL;  // stb_ds
    +static bool      sClipValid = false;
    +static int32_t   sLiveCount = 0;     // RichEdit controls alive: the last one gone frees the clipboard copy
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +static void        applyChange(WidgetT *w, const RichChangeT *change);
    +static int32_t     basFind(WidgetT *w, const char *text);
    +static bool        basGetSelBold(const WidgetT *w);
    +static int32_t     basGetSelColor(const WidgetT *w);
    +static bool        basGetSelItalic(const WidgetT *w);
    +static bool        basGetSelStrike(const WidgetT *w);
    +static bool        basGetSelUnderline(const WidgetT *w);
    +static void        basSetSelBold(WidgetT *w, bool on);
    +static void        basSetSelColor(WidgetT *w, int32_t basicColor);
    +static void        basSetSelFontName(WidgetT *w, const char *family);
    +static void        basSetSelFontSize(WidgetT *w, int32_t size);
    +static void        basSetSelItalic(WidgetT *w, bool on);
    +static void        basSetSelStrike(WidgetT *w, bool on);
    +static void        basSetSelUnderline(WidgetT *w, bool on);
    +static void        caretAt(WidgetT *w, int32_t offset, int32_t *x, int32_t *y, int32_t *h);
    +static void        dirtyOffsets(WidgetT *w, int32_t from, int32_t to);
    +static void        dirtyRows(RichEditDataT *ed, int32_t top, int32_t bottom);
    +static void        docFromPlain(RichEditDataT *ed, const char *text, int32_t len);
    +static void        docLoad(WidgetT *w, const char *text, int32_t len);
    +static void        docReplace(WidgetT *w, int32_t from, int32_t to, const RichTextT *what);
    +static void        docReset(WidgetT *w);
    +static void        docStart(WidgetT *w);
    +static void        docSync(WidgetT *w);
    +static bool        editReplace(WidgetT *w, int32_t from, int32_t to, const RichTextT *what, bool typing);
    +static void        fontsClose(RichEditDataT *ed);
    +static void        geomOf(const WidgetT *w, RichGeomT *g);
    +static int32_t     hitTest(WidgetT *w, int32_t vx, int32_t vy);
    +static void        insertPlain(WidgetT *w, const char *text, int32_t len, bool typing);
    +static void        moveTo(WidgetT *w, int32_t offset, bool extend);
    +static void        notifySel(WidgetT *w);
    +static void        paintRows(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t top, int32_t bottom);
    +static int32_t     paraAt(const RichEditDataT *ed, int32_t offset);
    +static int32_t     paraAtY(const RichEditDataT *ed, int32_t docY);
    +static void        paraBuild(WidgetT *w, int32_t p);
    +static void        parasFrom(RichEditDataT *ed, int32_t first, int32_t start, int32_t count);
    +static void        refresh(WidgetT *w);
    +static RichEditDataT *richData(const WidgetT *w);
    +static bool        scrollToCaret(WidgetT *w);
    +static void        selRange(const RichEditDataT *ed, int32_t *a, int32_t *b);
    +static int32_t     selStyle(const RichEditDataT *ed);
    +static BitmapFontT *styleFont(WidgetT *w, int32_t style);
    +static int32_t     typingStyle(const RichEditDataT *ed);
    +static void        undoApply(WidgetT *w, RichUndoT **from, RichUndoT **to);
    +static void        undoClear(RichUndoT **stack);
    +static void        undoFree(RichUndoT *u);
    +static void        undoPush(RichUndoT **stack, RichUndoT *u);
    +WidgetT           *wgtRichEdit(WidgetT *parent);
    +bool               wgtRichEditCanRedo(const WidgetT *w);
    +bool               wgtRichEditCanUndo(const WidgetT *w);
    +void               wgtRichEditCopy(WidgetT *w);
    +void               wgtRichEditCut(WidgetT *w);
    +int32_t            wgtRichEditFind(WidgetT *w, const char *text, bool matchCase);
    +const char        *wgtRichEditGetRtf(const WidgetT *w);
    +int32_t            wgtRichEditGetSelAlign(const WidgetT *w);
    +uint32_t           wgtRichEditGetSelColor(const WidgetT *w);
    +const char        *wgtRichEditGetSelFontName(const WidgetT *w);
    +int32_t            wgtRichEditGetSelFontSize(const WidgetT *w);
    +int32_t            wgtRichEditGetSelLength(const WidgetT *w);
    +const char        *wgtRichEditGetSelRtf(const WidgetT *w);
    +int32_t            wgtRichEditGetSelStart(const WidgetT *w);
    +uint32_t           wgtRichEditGetSelStyle(const WidgetT *w);
    +const char        *wgtRichEditGetSelText(const WidgetT *w);
    +int32_t            wgtRichEditLoadFile(WidgetT *w, const char *path);
    +void               wgtRichEditPaste(WidgetT *w);
    +void               wgtRichEditRedo(WidgetT *w);
    +int32_t            wgtRichEditSaveFile(WidgetT *w, const char *path);
    +void               wgtRichEditSelectAll(WidgetT *w);
    +void               wgtRichEditSetRtf(WidgetT *w, const char *rtf);
    +void               wgtRichEditSetSelAlign(WidgetT *w, int32_t align);
    +void               wgtRichEditSetSelColor(WidgetT *w, uint32_t rgb);
    +void               wgtRichEditSetSelFont(WidgetT *w, const char *family, int32_t size);
    +void               wgtRichEditSetSelLength(WidgetT *w, int32_t len);
    +void               wgtRichEditSetSelRtf(WidgetT *w, const char *rtf);
    +void               wgtRichEditSetSelStart(WidgetT *w, int32_t start);
    +void               wgtRichEditSetSelStyle(WidgetT *w, uint32_t style, uint32_t mask);
    +void               wgtRichEditSetSelText(WidgetT *w, const char *text);
    +void               wgtRichEditUndo(WidgetT *w);
    +void               widgetRichEditCalcMinSize(WidgetT *w, const BitmapFontT *font);
    +bool               widgetRichEditClearSelection(WidgetT *w);
    +void               widgetRichEditDestroy(WidgetT *w);
    +const char        *widgetRichEditGetText(const WidgetT *w);
    +void               widgetRichEditOnBlur(WidgetT *w);
    +void               widgetRichEditOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
    +void               widgetRichEditOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
    +void               widgetRichEditOnFocus(WidgetT *w);
    +void               widgetRichEditOnKey(WidgetT *w, int32_t key, int32_t mod);
    +void               widgetRichEditOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
    +bool               widgetRichEditOnWheel(WidgetT *w, int32_t delta);
    +void               widgetRichEditPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
    +void               widgetRichEditPoll(WidgetT *w, WindowT *win);
    +int32_t            widgetRichEditQuickRepaint(WidgetT *w, int32_t *outY, int32_t *outH);
    +void               widgetRichEditSetText(WidgetT *w, const char *text);
    +void               wgtRegister(void);
    +
    +
    +// A style change to the selection, or with none, to the style typing
    +// uses next
    +static void applyChange(WidgetT *w, const RichChangeT *change) {
    +    RichEditDataT *ed = richData(w);
    +    int32_t        a;
    +    int32_t        b;
    +
    +    selRange(ed, &a, &b);
    +
    +    if (a == b) {
    +        RichStyleT next = richStyleChanged(&ed->doc.styles[typingStyle(ed)], change);
    +
    +        ed->typingStyle = richTextIntern(&ed->doc, &next);
    +        return;
    +    }
    +
    +    if (w->readOnly) {
    +        return;
    +    }
    +
    +    RichTextT piece;
    +
    +    richTextInit(&piece);
    +    richTextCopy(&ed->doc, a, b, &piece);
    +    richTextRestyle(&piece, 0, b - a, change);
    +
    +    if (editReplace(w, a, b, &piece, false)) {
    +        RichEditDataT *after = richData(w);
    +
    +        after->anchor = a;
    +        after->caret  = b;
    +        dirtyOffsets(w, a, b);
    +        refresh(w);
    +    }
    +
    +    richTextFree(&piece);
    +}
    +
    +
    +// Find in BASIC: any case
    +static int32_t basFind(WidgetT *w, const char *text) {
    +    return wgtRichEditFind(w, text, false);
    +}
    +
    +
    +static bool basGetSelBold(const WidgetT *w) {
    +    return (wgtRichEditGetSelStyle(w) & FONT_STYLE_BOLD) != 0;
    +}
    +
    +
    +// SelColor in BASIC: a BASIC color (&HBBGGRR), -1 for the ForeColor
    +static int32_t basGetSelColor(const WidgetT *w) {
    +    uint32_t rgb = wgtRichEditGetSelColor(w);
    +
    +    return (rgb == RICHEDIT_COLOR_DEFAULT) ? BAS_COLOR_DEFAULT : (int32_t)wgtBasicColor(rgb);
    +}
    +
    +
    +static bool basGetSelItalic(const WidgetT *w) {
    +    return (wgtRichEditGetSelStyle(w) & FONT_STYLE_ITALIC) != 0;
    +}
    +
    +
    +static bool basGetSelStrike(const WidgetT *w) {
    +    return (wgtRichEditGetSelStyle(w) & FONT_STYLE_STRIKE) != 0;
    +}
    +
    +
    +static bool basGetSelUnderline(const WidgetT *w) {
    +    return (wgtRichEditGetSelStyle(w) & FONT_STYLE_UNDERLINE) != 0;
    +}
    +
    +
    +static void basSetSelBold(WidgetT *w, bool on) {
    +    wgtRichEditSetSelStyle(w, on ? FONT_STYLE_BOLD : 0, FONT_STYLE_BOLD);
    +}
    +
    +
    +static void basSetSelColor(WidgetT *w, int32_t basicColor) {
    +    wgtRichEditSetSelColor(w, (basicColor == BAS_COLOR_DEFAULT) ? RICHEDIT_COLOR_DEFAULT : wgtBasicColor((uint32_t)basicColor));
    +}
    +
    +
    +static void basSetSelFontName(WidgetT *w, const char *family) {
    +    wgtRichEditSetSelFont(w, family, 0);
    +}
    +
    +
    +static void basSetSelFontSize(WidgetT *w, int32_t size) {
    +    wgtRichEditSetSelFont(w, NULL, size);
    +}
    +
    +
    +static void basSetSelItalic(WidgetT *w, bool on) {
    +    wgtRichEditSetSelStyle(w, on ? FONT_STYLE_ITALIC : 0, FONT_STYLE_ITALIC);
    +}
    +
    +
    +static void basSetSelStrike(WidgetT *w, bool on) {
    +    wgtRichEditSetSelStyle(w, on ? FONT_STYLE_STRIKE : 0, FONT_STYLE_STRIKE);
    +}
    +
    +
    +static void basSetSelUnderline(WidgetT *w, bool on) {
    +    wgtRichEditSetSelStyle(w, on ? FONT_STYLE_UNDERLINE : 0, FONT_STYLE_UNDERLINE);
    +}
    +
    +
    +// The caret before offset, in document pixels: x from the text's left,
    +// y from the document's top, and its line's height
    +static void caretAt(WidgetT *w, int32_t offset, int32_t *x, int32_t *y, int32_t *h) {
    +    RichEditDataT   *ed = richData(w);
    +    int32_t          p  = paraAt(ed, offset);
    +    const RichParaT *pr = &ed->paras[p];
    +
    +    dvxFlowCaret(pr->flow, offset - pr->start, x, y, h);
    +    *y += pr->y;
    +}
    +
    +
    +// The rows of the lines from offset from to offset to, to quick-repaint
    +static void dirtyOffsets(WidgetT *w, int32_t from, int32_t to) {
    +    RichEditDataT *ed = richData(w);
    +    int32_t        x;
    +    int32_t        y0;
    +    int32_t        y1;
    +    int32_t        h0;
    +    int32_t        h1;
    +
    +    caretAt(w, DVX_MIN(from, to), &x, &y0, &h0);
    +    caretAt(w, DVX_MAX(from, to), &x, &y1, &h1);
    +    dirtyRows(ed, y0, y1 + h1);
    +}
    +
    +
    +static void dirtyRows(RichEditDataT *ed, int32_t top, int32_t bottom) {
    +    if (ed->dirtyTop >= ed->dirtyBottom) {
    +        ed->dirtyTop    = top;
    +        ed->dirtyBottom = bottom;
    +        return;
    +    }
    +
    +    ed->dirtyTop    = DVX_MIN(ed->dirtyTop, top);
    +    ed->dirtyBottom = DVX_MAX(ed->dirtyBottom, bottom);
    +}
    +
    +
    +// The document as plain text in the default style
    +static void docFromPlain(RichEditDataT *ed, const char *text, int32_t len) {
    +    RichStyleT def;
    +
    +    memset(&def, 0, sizeof(def));
    +    def.color = RICH_COLOR_DEFAULT;
    +    richTextFromPlain(&ed->doc, text, len, &def);
    +}
    +
    +
    +// The whole document from text: RTF when it is, else plain text
    +static void docLoad(WidgetT *w, const char *text, int32_t len) {
    +    RichEditDataT *ed = richData(w);
    +
    +    if (!richRtfRead(&ed->doc, text, len)) {
    +        docFromPlain(ed, text ? text : "", text ? len : 0);
    +    }
    +
    +    docStart(w);
    +}
    +
    +
    +// Replace offsets from to to with what (NULL: delete) in the document
    +// and its paragraphs: those the change touched are laid out again, those
    +// after it move; a paragraph that changed height repaints the whole
    +// control, else the touched rows do
    +static void docReplace(WidgetT *w, int32_t from, int32_t to, const RichTextT *what) {
    +    RichEditDataT *ed      = richData(w);
    +    int32_t        first   = paraAt(ed, from);
    +    int32_t        last    = paraAt(ed, to);
    +    int32_t        oldN    = (int32_t)arrlen(ed->paras);
    +    int32_t        oldTop  = ed->paras[first].y;
    +    int32_t        oldBot  = ed->paras[last].y + ed->paras[last].height;
    +    int32_t        start   = ed->paras[first].start;
    +    int32_t        added   = richTextReplace(&ed->doc, from, to, what);
    +    int32_t        delta   = added - (to - from);
    +    int32_t        newN    = richTextParaCount(&ed->doc);
    +    int32_t        count   = newN - (oldN - (last - first + 1));
    +
    +    for (int32_t p = first; p <= last; p++) {
    +        dvxFlowDestroy(ed->paras[p].flow);
    +    }
    +
    +    arrdeln(ed->paras, first, last - first + 1);
    +    arrinsn(ed->paras, first, count);
    +    memset(&ed->paras[first], 0, sizeof(RichParaT) * (size_t)count);
    +    parasFrom(ed, first, start, count);
    +
    +    for (int32_t p = first + count; p < arrlen(ed->paras); p++) {
    +        ed->paras[p].start += delta;
    +    }
    +
    +    // Each style may now have a font to open
    +    while (arrlen(ed->fonts) < arrlen(ed->doc.styles)) {
    +        arrput(ed->fonts, NULL);
    +    }
    +
    +    docSync(w);
    +
    +    // The same rows hold the change: repaint them; else everything below
    +    // moved
    +    int32_t newBot = ed->paras[first + count - 1].y + ed->paras[first + count - 1].height;
    +
    +    if (count != last - first + 1 || newBot != oldBot) {
    +        ed->fullPaint = true;
    +    } else {
    +        dirtyRows(ed, oldTop, newBot);
    +    }
    +}
    +
    +
    +// The document's paragraphs all laid out again (a new text, font or width)
    +static void docReset(WidgetT *w) {
    +    RichEditDataT *ed = richData(w);
    +
    +    for (int32_t p = 0; p < arrlen(ed->paras); p++) {
    +        dvxFlowDestroy(ed->paras[p].flow);
    +    }
    +
    +    arrsetlen(ed->paras, richTextParaCount(&ed->doc));
    +    memset(ed->paras, 0, sizeof(RichParaT) * (size_t)arrlen(ed->paras));
    +    parasFrom(ed, 0, 0, (int32_t)arrlen(ed->paras));
    +    fontsClose(ed);
    +    arrsetlen(ed->fonts, arrlen(ed->doc.styles));
    +    memset(ed->fonts, 0, sizeof(BitmapFontT *) * (size_t)arrlen(ed->fonts));
    +    ed->layoutW   = 0;
    +    ed->fullPaint = true;
    +    docSync(w);
    +}
    +
    +
    +// A new document in place: the caret to the start, nothing scrolled,
    +// the undo steps forgotten, everything laid out and painted again
    +static void docStart(WidgetT *w) {
    +    RichEditDataT *ed = richData(w);
    +
    +    undoClear(&ed->undo);
    +    undoClear(&ed->redo);
    +    ed->anchor      = 0;
    +    ed->caret       = 0;
    +    ed->scrollY     = 0;
    +    ed->typingStyle = RICH_NONE;
    +    ed->wantX       = RICH_NONE;
    +    docReset(w);
    +    refresh(w);
    +}
    +
    +
    +// The paragraphs laid out for the control's font and width now: a new
    +// font (or one built again) opens every style's font again, a new width
    +// lays every paragraph out again, else only those to do; then the tops
    +static void docSync(WidgetT *w) {
    +    RichEditDataT     *ed  = richData(w);
    +    const BitmapFontT *own = wgtFont(w);
    +    RichGeomT          g;
    +    bool               all = false;
    +
    +    geomOf(w, &g);
    +
    +    if (!fontStampIs(&ed->ownFont, own)) {
    +        fontsClose(ed);
    +        arrsetlen(ed->fonts, arrlen(ed->doc.styles));
    +        memset(ed->fonts, 0, sizeof(BitmapFontT *) * (size_t)arrlen(ed->fonts));
    +        fontStampSet(&ed->ownFont, own);
    +        all = true;
    +    }
    +
    +    if (g.textW != ed->layoutW) {
    +        ed->layoutW = g.textW;
    +        all         = true;
    +    }
    +
    +    int32_t y = 0;
    +
    +    for (int32_t p = 0; p < arrlen(ed->paras); p++) {
    +        RichParaT *pr = &ed->paras[p];
    +
    +        if (all || !pr->flow) {
    +            paraBuild(w, p);
    +        }
    +
    +        pr->y      = y;
    +        pr->height = dvxFlowLayout(pr->flow, ed->layoutW);
    +        y         += pr->height;
    +    }
    +
    +    if (all) {
    +        ed->fullPaint = true;
    +    }
    +
    +    ed->docH = y;
    +}
    +
    +
    +// Replace from to to with what as one undo step (typing: typed
    +// characters, joined to the step before when it was typing that ended
    +// here); the caret after what.  False when the Change handler destroyed
    +// the control.
    +static bool editReplace(WidgetT *w, int32_t from, int32_t to, const RichTextT *what, bool typing) {
    +    RichEditDataT *ed    = richData(w);
    +    int32_t        added = what ? richTextLength(what) : 0;
    +    int32_t        n     = (int32_t)arrlen(ed->undo);
    +
    +    if (typing && to == from && n > 0 && ed->undo[n - 1].typing && ed->undo[n - 1].at + ed->undo[n - 1].len == from) {
    +        ed->undo[n - 1].len += added;
    +    } else {
    +        RichUndoT u;
    +
    +        memset(&u, 0, sizeof(u));
    +        richTextInit(&u.was);
    +        richTextCopy(&ed->doc, from, to, &u.was);
    +        u.at     = from;
    +        u.len    = added;
    +        u.anchor = ed->anchor;
    +        u.caret  = ed->caret;
    +        u.typing = typing;
    +        undoPush(&ed->undo, &u);
    +    }
    +
    +    undoClear(&ed->redo);
    +    dirtyOffsets(w, ed->anchor, ed->caret);
    +    docReplace(w, from, to, what);
    +    ed->anchor      = from + added;
    +    ed->caret       = from + added;
    +    ed->wantX       = RICH_NONE;
    +
    +    if (!typing) {
    +        ed->typingStyle = RICH_NONE;
    +    }
    +
    +    dirtyOffsets(w, ed->caret, ed->caret);
    +    scrollToCaret(w);
    +    refresh(w);
    +    return widgetFireChange(w);
    +}
    +
    +
    +static void fontsClose(RichEditDataT *ed) {
    +    for (int32_t i = 0; i < arrlen(ed->fonts); i++) {
    +        dvxFontClose(ed->fonts[i]);
    +    }
    +
    +    arrsetlen(ed->fonts, 0);
    +}
    +
    +
    +static void geomOf(const WidgetT *w, RichGeomT *g) {
    +    const RichEditDataT *ed = richData(w);
    +
    +    g->sbX   = w->x + w->w - RICH_BORDER - WGT_SB_W;
    +    g->sbY   = w->y + RICH_BORDER;
    +    g->sbH   = w->h - RICH_BORDER * 2;
    +    g->textX = w->x + RICH_BORDER + RICH_PAD;
    +    g->textW = (w->w > 0) ? DVX_MAX(g->sbX - RICH_PAD - g->textX, 1) : FLOW_WIDTH_NATURAL;
    +    g->viewY = w->y + RICH_BORDER;
    +    g->viewH = DVX_MAX(w->h - RICH_BORDER * 2, 0);
    +    g->textH = g->viewH - RICH_PAD * 2;
    +    g->textY = g->viewY + RICH_PAD - ed->scrollY;
    +}
    +
    +
    +// The offset nearest screen point vx, vy
    +static int32_t hitTest(WidgetT *w, int32_t vx, int32_t vy) {
    +    RichEditDataT   *ed = richData(w);
    +    RichGeomT        g;
    +
    +    geomOf(w, &g);
    +
    +    int32_t          docY = vy - g.textY;
    +    int32_t          p    = paraAtY(ed, docY);
    +    const RichParaT *pr   = &ed->paras[p];
    +
    +    return pr->start + dvxFlowHitTest(pr->flow, vx - g.textX, docY - pr->y);
    +}
    +
    +
    +// Plain text over the selection, in the style typing uses (typing: one
    +// typed character)
    +static void insertPlain(WidgetT *w, const char *text, int32_t len, bool typing) {
    +    RichEditDataT *ed = richData(w);
    +    RichTextT      piece;
    +    int32_t        a;
    +    int32_t        b;
    +
    +    if (w->readOnly) {
    +        return;
    +    }
    +
    +    selRange(ed, &a, &b);
    +    richTextInit(&piece);
    +    piece.styles[0] = ed->doc.styles[typingStyle(ed)];
    +    piece.aligns[0] = richTextAlign(&ed->doc, richTextParaOf(&ed->doc, a));
    +    richTextInsertPlain(&piece, 0, text, len, 0);
    +    editReplace(w, a, b, &piece, typing && a == b);
    +    richTextFree(&piece);
    +}
    +
    +
    +// The caret to offset (extend: the selection's anchor stays), the rows it
    +// left and reached repainted, and scrolled into view
    +static void moveTo(WidgetT *w, int32_t offset, bool extend) {
    +    RichEditDataT *ed = richData(w);
    +
    +    offset = DVX_MAX(0, DVX_MIN(offset, richTextLength(&ed->doc)));
    +    dirtyOffsets(w, ed->anchor, ed->caret);
    +    ed->caret       = offset;
    +    ed->anchor      = extend ? ed->anchor : offset;
    +    ed->typingStyle = RICH_NONE;
    +    ed->caretOn     = true;
    +    ed->blinkAt     = platformClock();
    +    dirtyOffsets(w, ed->anchor, ed->caret);
    +    scrollToCaret(w);
    +    refresh(w);
    +    notifySel(w);
    +}
    +
    +
    +static void notifySel(WidgetT *w) {
    +    wgtFireEvent(w, RICHEDIT_EVT_SELCHANGE, NULL, 0);
    +}
    +
    +
    +// The document rows top to bottom (clipped to the text area by the caller):
    +// the background, the paragraphs with the selection, the caret
    +static void paintRows(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t top, int32_t bottom) {
    +    RichEditDataT *ed     = richData(w);
    +    uint32_t       fg     = w->fgColor ? w->fgColor : colors->contentFg;
    +    uint32_t       bg     = w->bgColor ? w->bgColor : colors->contentBg;
    +    bool           active = w == sFocusedWidget;
    +    RichGeomT      g;
    +    int32_t        a;
    +    int32_t        b;
    +
    +    geomOf(w, &g);
    +    selRange(ed, &a, &b);
    +    rectFill(d, ops, w->x + RICH_BORDER, g.textY + top, g.sbX - w->x - RICH_BORDER, bottom - top, bg);
    +
    +    for (int32_t p = paraAtY(ed, top); p < arrlen(ed->paras) && ed->paras[p].y < bottom; p++) {
    +        const RichParaT *pr = &ed->paras[p];
    +
    +        dvxFlowPaintSel(pr->flow, d, ops, g.textX, g.textY + pr->y, fg, a - pr->start, b - pr->start, colors->menuHighlightFg, colors->menuHighlightBg);
    +
    +        // A selection past the paragraph's end shows its end too
    +        if (a <= pr->start + pr->len && b > pr->start + pr->len) {
    +            int32_t x;
    +            int32_t y;
    +            int32_t h;
    +
    +            dvxFlowCaret(pr->flow, pr->len, &x, &y, &h);
    +            rectFill(d, ops, g.textX + x, g.textY + pr->y + y, textWidthN(wgtFont(w), " ", 1), h, colors->menuHighlightBg);
    +        }
    +    }
    +
    +    if (active && ed->caretOn && w->enabled) {
    +        int32_t x;
    +        int32_t y;
    +        int32_t h;
    +
    +        caretAt(w, ed->caret, &x, &y, &h);
    +        rectFill(d, ops, g.textX + x, g.textY + y, RICH_CARET_W, h, fg);
    +    }
    +}
    +
    +
    +// The paragraph offset is in (the last one at the end)
    +static int32_t paraAt(const RichEditDataT *ed, int32_t offset) {
    +    int32_t lo = 0;
    +    int32_t hi = (int32_t)arrlen(ed->paras) - 1;
    +
    +    while (lo < hi) {
    +        int32_t mid = (lo + hi + 1) / 2;
    +
    +        if (ed->paras[mid].start <= offset) {
    +            lo = mid;
    +        } else {
    +            hi = mid - 1;
    +        }
    +    }
    +
    +    return lo;
    +}
    +
    +
    +// The paragraph at document row docY (the first above, the last below)
    +static int32_t paraAtY(const RichEditDataT *ed, int32_t docY) {
    +    int32_t lo = 0;
    +    int32_t hi = (int32_t)arrlen(ed->paras) - 1;
    +
    +    while (lo < hi) {
    +        int32_t mid = (lo + hi + 1) / 2;
    +
    +        if (ed->paras[mid].y <= docY) {
    +            lo = mid;
    +        } else {
    +            hi = mid - 1;
    +        }
    +    }
    +
    +    return lo;
    +}
    +
    +
    +// Paragraph p's flow from the document: its runs in their styles' fonts
    +// and colors (an empty paragraph: its style's font, for its height)
    +static void paraBuild(WidgetT *w, int32_t p) {
    +    RichEditDataT *ed  = richData(w);
    +    RichParaT     *pr  = &ed->paras[p];
    +    int32_t        end = pr->start + pr->len;
    +    int32_t        run = richTextRunAt(&ed->doc, pr->start);
    +
    +    if (!pr->flow) {
    +        pr->flow = dvxFlowCreate();
    +    } else {
    +        dvxFlowClear(pr->flow);
    +    }
    +
    +    dvxFlowAddParagraph(pr->flow, richTextAlign(&ed->doc, p));
    +
    +    if (pr->len == 0) {
    +        int32_t style = ed->doc.runs[run].style;
    +
    +        dvxFlowAddText(pr->flow, "", 0, styleFont(w, style), ed->doc.styles[style].color);
    +        return;
    +    }
    +
    +    for (int32_t pos = pr->start; pos < end; run++) {
    +        int32_t next  = (run + 1 < arrlen(ed->doc.runs)) ? DVX_MIN(ed->doc.runs[run + 1].start, end) : end;
    +        int32_t style = ed->doc.runs[run].style;
    +
    +        dvxFlowAddText(pr->flow, ed->doc.text + pos, next - pos, styleFont(w, style), ed->doc.styles[style].color);
    +        pos = next;
    +    }
    +}
    +
    +
    +// count paragraph entries from first on, the first at offset start: their
    +// starts and lengths from the document's text
    +static void parasFrom(RichEditDataT *ed, int32_t first, int32_t start, int32_t count) {
    +    int32_t len = richTextLength(&ed->doc);
    +    int32_t at  = start;
    +
    +    for (int32_t p = first; p < first + count; p++) {
    +        int32_t end = at;
    +
    +        while (end < len && ed->doc.text[end] != RICH_CHAR_NEWLINE) {
    +            end++;
    +        }
    +
    +        ed->paras[p].start = at;
    +        ed->paras[p].len   = end - at;
    +        at                 = end + 1;
    +    }
    +}
    +
    +
    +// The whole control to paint (the quick repaint does a band)
    +static void refresh(WidgetT *w) {
    +    RichEditDataT *ed = richData(w);
    +
    +    if (ed->fullPaint) {
    +        ed->fullPaint   = false;
    +        ed->dirtyTop    = 0;
    +        ed->dirtyBottom = 0;
    +        wgtInvalidatePaint(w);
    +    }
    +}
    +
    +
    +static RichEditDataT *richData(const WidgetT *w) {
    +    return (RichEditDataT *)w->data;
    +}
    +
    +
    +// Scroll so the caret's line shows; true when it scrolled
    +static bool scrollToCaret(WidgetT *w) {
    +    RichEditDataT *ed = richData(w);
    +    RichGeomT      g;
    +    int32_t        x;
    +    int32_t        y;
    +    int32_t        h;
    +
    +    geomOf(w, &g);
    +    caretAt(w, ed->caret, &x, &y, &h);
    +    int32_t old  = ed->scrollY;
    +
    +    if (y < ed->scrollY) {
    +        ed->scrollY = y;
    +    } else if (y + h > ed->scrollY + g.textH) {
    +        ed->scrollY = y + h - g.textH;
    +    }
    +
    +    ed->scrollY = DVX_MAX(0, DVX_MIN(ed->scrollY, DVX_MAX(ed->docH - g.textH, 0)));
    +
    +    if (ed->scrollY == old) {
    +        return false;
    +    }
    +
    +    ed->fullPaint = true;
    +    return true;
    +}
    +
    +
    +static void selRange(const RichEditDataT *ed, int32_t *a, int32_t *b) {
    +    *a = DVX_MIN(ed->anchor, ed->caret);
    +    *b = DVX_MAX(ed->anchor, ed->caret);
    +}
    +
    +
    +// The style of the selection's first character; with none selected, the
    +// one typing uses
    +static int32_t selStyle(const RichEditDataT *ed) {
    +    int32_t a;
    +    int32_t b;
    +
    +    selRange(ed, &a, &b);
    +    return (a == b) ? typingStyle(ed) : richTextStyleAt(&ed->doc, a);
    +}
    +
    +
    +// The font of document style style: its family and size, or the control's
    +static BitmapFontT *styleFont(WidgetT *w, int32_t style) {
    +    RichEditDataT     *ed  = richData(w);
    +    const RichStyleT  *s   = &ed->doc.styles[style];
    +    const BitmapFontT *own = wgtFont(w);
    +
    +    while (arrlen(ed->fonts) <= style) {
    +        arrput(ed->fonts, NULL);
    +    }
    +
    +    if (!ed->fonts[style]) {
    +        ed->fonts[style] = dvxFontOpen(s->family[0] ? s->family : dvxFontFamilyOf(own), s->size ? s->size : own->pixelSize, s->style);
    +    }
    +
    +    return ed->fonts[style] ? ed->fonts[style] : (BitmapFontT *)own;
    +}
    +
    +
    +// The style typing uses: one chosen, else the character's before the
    +// caret in its paragraph, else the one at it
    +static int32_t typingStyle(const RichEditDataT *ed) {
    +    if (ed->typingStyle != RICH_NONE) {
    +        return ed->typingStyle;
    +    }
    +
    +    int32_t at = ed->caret;
    +
    +    if (at > 0 && ed->doc.text[at - 1] != RICH_CHAR_NEWLINE) {
    +        at--;
    +    }
    +
    +    return richTextStyleAt(&ed->doc, at);
    +}
    +
    +
    +// Undo (from: the undo stack) or redo (from: the redo stack) a step, the
    +// step that takes it back pushed on to
    +static void undoApply(WidgetT *w, RichUndoT **from, RichUndoT **to) {
    +    RichEditDataT *ed = richData(w);
    +    RichUndoT      u;
    +    RichUndoT      back;
    +
    +    if (w->readOnly || arrlen(*from) == 0) {
    +        return;
    +    }
    +
    +    u = arrpop(*from);
    +    memset(&back, 0, sizeof(back));
    +    back.anchor = ed->anchor;
    +    back.caret  = ed->caret;
    +    back.at     = u.at;
    +    back.para   = u.para;
    +    richTextInit(&back.was);
    +
    +    if (u.aligns) {
    +        for (int32_t i = 0; i < arrlen(u.aligns); i++) {
    +            arrput(back.aligns, richTextAlign(&ed->doc, u.para + i));
    +            richTextSetAlign(&ed->doc, u.para + i, u.para + i, u.aligns[i]);
    +            dvxFlowDestroy(ed->paras[u.para + i].flow);
    +            ed->paras[u.para + i].flow = NULL;
    +        }
    +
    +        docSync(w);
    +        ed->fullPaint = true;
    +    } else {
    +        richTextCopy(&ed->doc, u.at, u.at + u.len, &back.was);
    +        back.len = richTextLength(&u.was);
    +        docReplace(w, u.at, u.at + u.len, &u.was);
    +    }
    +
    +    undoPush(to, &back);
    +    ed->anchor      = DVX_MIN(u.anchor, richTextLength(&ed->doc));
    +    ed->caret       = DVX_MIN(u.caret, richTextLength(&ed->doc));
    +    ed->typingStyle = RICH_NONE;
    +    ed->wantX       = RICH_NONE;
    +    undoFree(&u);
    +    ed->fullPaint = true;
    +    scrollToCaret(w);
    +    refresh(w);
    +
    +    if (widgetFireChange(w)) {
    +        notifySel(w);
    +    }
    +}
    +
    +
    +static void undoClear(RichUndoT **stack) {
    +    for (int32_t i = 0; i < arrlen(*stack); i++) {
    +        undoFree(&(*stack)[i]);
    +    }
    +
    +    arrsetlen(*stack, 0);
    +}
    +
    +
    +static void undoFree(RichUndoT *u) {
    +    richTextFree(&u->was);
    +    arrfree(u->aligns);
    +}
    +
    +
    +// u on stack (it holds u's arrays now), the oldest step dropped past
    +// RICH_UNDO_MAX
    +static void undoPush(RichUndoT **stack, RichUndoT *u) {
    +    if (arrlen(*stack) >= RICH_UNDO_MAX) {
    +        undoFree(&(*stack)[0]);
    +        arrdel(*stack, 0);
    +    }
    +
    +    arrput(*stack, *u);
    +}
    +
    +
    +WidgetT *wgtRichEdit(WidgetT *parent) {
    +    WidgetT *w = widgetAlloc(parent, sTypeId);
    +
    +    if (!w) {
    +        return NULL;
    +    }
    +
    +    RichEditDataT *ed = (RichEditDataT *)calloc(1, sizeof(RichEditDataT));
    +
    +    if (!ed) {
    +        widgetAllocRollback(w);
    +        return NULL;
    +    }
    +
    +    sLiveCount++;
    +    richTextInit(&ed->doc);
    +    ed->wantX       = RICH_NONE;
    +    ed->typingStyle = RICH_NONE;
    +    ed->caretOn     = true;
    +    w->data         = ed;
    +    docReset(w);
    +    return w;
    +}
    +
    +
    +bool wgtRichEditCanRedo(const WidgetT *w) {
    +    VALIDATE_WIDGET(w, sTypeId, false);
    +    return arrlen(richData(w)->redo) > 0;
    +}
    +
    +
    +bool wgtRichEditCanUndo(const WidgetT *w) {
    +    VALIDATE_WIDGET(w, sTypeId, false);
    +    return arrlen(richData(w)->undo) > 0;
    +}
    +
    +
    +// The selection on the clipboard as plain text, kept styled here
    +void wgtRichEditCopy(WidgetT *w) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichEditDataT *ed = richData(w);
    +    int32_t        a;
    +    int32_t        b;
    +
    +    selRange(ed, &a, &b);
    +
    +    if (a == b) {
    +        return;
    +    }
    +
    +    if (!sClipValid) {
    +        richTextInit(&sClip);
    +        sClipValid = true;
    +    }
    +
    +    richTextCopy(&ed->doc, a, b, &sClip);
    +    arrsetlen(sClipText, b - a);
    +    memcpy(sClipText, ed->doc.text + a, (size_t)(b - a));
    +    dvxClipboardCopy(sClipText, b - a);
    +}
    +
    +
    +void wgtRichEditCut(WidgetT *w) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichEditDataT *ed = richData(w);
    +    int32_t        a;
    +    int32_t        b;
    +
    +    selRange(ed, &a, &b);
    +
    +    if (a == b || w->readOnly) {
    +        return;
    +    }
    +
    +    wgtRichEditCopy(w);
    +    editReplace(w, a, b, NULL, false);
    +}
    +
    +
    +// The next text after the selection (from the start past the end), any
    +// case unless matchCase: selected; its offset, -1 for none
    +int32_t wgtRichEditFind(WidgetT *w, const char *text, bool matchCase) {
    +    VALIDATE_WIDGET(w, sTypeId, RICH_NONE);
    +    RichEditDataT *ed  = richData(w);
    +    int32_t        len = richTextLength(&ed->doc);
    +    int32_t        n   = text ? (int32_t)strlen(text) : 0;
    +    int32_t        a;
    +    int32_t        b;
    +
    +    selRange(ed, &a, &b);
    +
    +    if (n == 0 || n > len) {
    +        return RICH_NONE;
    +    }
    +
    +    // From the selection's end, around past the last place text fits
    +    int32_t places = len - n + 1;
    +    int32_t from   = (b < places) ? b : 0;
    +
    +    for (int32_t i = 0; i < places; i++) {
    +        int32_t at = (from + i) % places;
    +        bool    hit = matchCase ? strncmp(ed->doc.text + at, text, (size_t)n) == 0 : strncasecmp(ed->doc.text + at, text, (size_t)n) == 0;
    +
    +        if (hit) {
    +            moveTo(w, at, false);
    +            moveTo(w, at + n, true);
    +            return at;
    +        }
    +    }
    +
    +    return RICH_NONE;
    +}
    +
    +
    +// The whole document as RTF (valid until the next call)
    +const char *wgtRichEditGetRtf(const WidgetT *w) {
    +    VALIDATE_WIDGET(w, sTypeId, "");
    +    RichEditDataT *ed = richData(w);
    +
    +    free(ed->rtfOut);
    +    ed->rtfOut = richRtfWrite(&ed->doc, dvxFontFamilyOf(wgtFont(w)), NULL);
    +    return ed->rtfOut ? ed->rtfOut : "";
    +}
    +
    +
    +int32_t wgtRichEditGetSelAlign(const WidgetT *w) {
    +    VALIDATE_WIDGET(w, sTypeId, FLOW_ALIGN_LEFT);
    +    const RichEditDataT *ed = richData(w);
    +
    +    return richTextAlign(&ed->doc, richTextParaOf(&ed->doc, DVX_MIN(ed->anchor, ed->caret)));
    +}
    +
    +
    +uint32_t wgtRichEditGetSelColor(const WidgetT *w) {
    +    VALIDATE_WIDGET(w, sTypeId, RICHEDIT_COLOR_DEFAULT);
    +    const RichEditDataT *ed = richData(w);
    +
    +    return ed->doc.styles[selStyle(ed)].color;
    +}
    +
    +
    +// The selection's family ("" when it is the control's own: the family
    +// the control draws it in)
    +const char *wgtRichEditGetSelFontName(const WidgetT *w) {
    +    VALIDATE_WIDGET(w, sTypeId, "");
    +    const RichEditDataT *ed = richData(w);
    +    const RichStyleT    *s  = &ed->doc.styles[selStyle(ed)];
    +
    +    return s->family[0] ? s->family : dvxFontFamilyOf(wgtFont(w));
    +}
    +
    +
    +int32_t wgtRichEditGetSelFontSize(const WidgetT *w) {
    +    VALIDATE_WIDGET(w, sTypeId, 0);
    +    const RichEditDataT *ed    = richData(w);
    +    int32_t              style = selStyle(ed);
    +
    +    return (style < arrlen(ed->fonts) && ed->fonts[style]) ? ed->fonts[style]->pixelSize : (ed->doc.styles[style].size ? ed->doc.styles[style].size : wgtFont(w)->pixelSize);
    +}
    +
    +
    +int32_t wgtRichEditGetSelLength(const WidgetT *w) {
    +    VALIDATE_WIDGET(w, sTypeId, 0);
    +    const RichEditDataT *ed = richData(w);
    +
    +    return abs(ed->caret - ed->anchor);
    +}
    +
    +
    +const char *wgtRichEditGetSelRtf(const WidgetT *w) {
    +    VALIDATE_WIDGET(w, sTypeId, "");
    +    RichEditDataT *ed = richData(w);
    +    RichTextT      piece;
    +    int32_t        a;
    +    int32_t        b;
    +
    +    selRange(ed, &a, &b);
    +    richTextInit(&piece);
    +    richTextCopy(&ed->doc, a, b, &piece);
    +    free(ed->rtfOut);
    +    ed->rtfOut = richRtfWrite(&piece, dvxFontFamilyOf(wgtFont(w)), NULL);
    +    richTextFree(&piece);
    +    return ed->rtfOut ? ed->rtfOut : "";
    +}
    +
    +
    +int32_t wgtRichEditGetSelStart(const WidgetT *w) {
    +    VALIDATE_WIDGET(w, sTypeId, 0);
    +    const RichEditDataT *ed = richData(w);
    +
    +    return DVX_MIN(ed->anchor, ed->caret);
    +}
    +
    +
    +uint32_t wgtRichEditGetSelStyle(const WidgetT *w) {
    +    VALIDATE_WIDGET(w, sTypeId, 0);
    +    const RichEditDataT *ed = richData(w);
    +
    +    return ed->doc.styles[selStyle(ed)].style;
    +}
    +
    +
    +// The selection's text (valid until the next call)
    +const char *wgtRichEditGetSelText(const WidgetT *w) {
    +    VALIDATE_WIDGET(w, sTypeId, "");
    +    RichEditDataT *ed = richData(w);
    +    int32_t        a;
    +    int32_t        b;
    +
    +    selRange(ed, &a, &b);
    +    arrsetlen(ed->textOut, b - a + 1);
    +
    +    if (b > a) {
    +        memcpy(ed->textOut, ed->doc.text + a, (size_t)(b - a));
    +    }
    +
    +    ed->textOut[b - a] = '\0';
    +    return ed->textOut;
    +}
    +
    +
    +// A file's text: RTF when it is, else plain text.  0, or -1 when it
    +// cannot be read.
    +int32_t wgtRichEditLoadFile(WidgetT *w, const char *path) {
    +    VALIDATE_WIDGET(w, sTypeId, RICH_NONE);
    +    int32_t len  = 0;
    +    char   *data = path ? platformReadFile(path, &len) : NULL;
    +
    +    if (!data) {
    +        return RICH_NONE;
    +    }
    +
    +    docLoad(w, data, len);
    +    free(data);
    +    return 0;
    +}
    +
    +
    +// The clipboard's text over the selection: with its styles when it is
    +// what Copy or Cut put there, else in the style typing uses
    +void wgtRichEditPaste(WidgetT *w) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichEditDataT *ed   = richData(w);
    +    int32_t        len  = 0;
    +    const char    *text = dvxClipboardGet(&len);
    +    int32_t        a;
    +    int32_t        b;
    +
    +    if (!text || len <= 0 || w->readOnly) {
    +        return;
    +    }
    +
    +    if (sClipValid && len == arrlen(sClipText) && memcmp(text, sClipText, (size_t)len) == 0) {
    +        selRange(ed, &a, &b);
    +        editReplace(w, a, b, &sClip, false);
    +        return;
    +    }
    +
    +    insertPlain(w, text, len, false);
    +}
    +
    +
    +void wgtRichEditRedo(WidgetT *w) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichEditDataT *ed = richData(w);
    +
    +    undoApply(w, &ed->redo, &ed->undo);
    +}
    +
    +
    +// The text as RTF when path ends in .rtf, else as plain text.  0, or -1
    +// when it cannot be written.
    +int32_t wgtRichEditSaveFile(WidgetT *w, const char *path) {
    +    VALIDATE_WIDGET(w, sTypeId, RICH_NONE);
    +    RichEditDataT *ed  = richData(w);
    +    size_t         n   = path ? strlen(path) : 0;
    +    bool           rtf = n >= strlen(RICH_EXT_RTF) && strcasecmp(path + n - strlen(RICH_EXT_RTF), RICH_EXT_RTF) == 0;
    +    FILE          *fp  = path ? fopen(path, rtf ? "wb" : "w") : NULL;
    +
    +    if (!fp) {
    +        return RICH_NONE;
    +    }
    +
    +    bool ok;
    +
    +    if (rtf) {
    +        const char *out = wgtRichEditGetRtf(w);
    +
    +        ok = fwrite(out, 1, strlen(out), fp) == strlen(out);
    +    } else {
    +        ok = fwrite(ed->doc.text, 1, (size_t)richTextLength(&ed->doc), fp) == (size_t)richTextLength(&ed->doc);
    +    }
    +
    +    ok = (fclose(fp) == 0) && ok;
    +    return ok ? 0 : RICH_NONE;
    +}
    +
    +
    +void wgtRichEditSelectAll(WidgetT *w) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    moveTo(w, 0, false);
    +    moveTo(w, richTextLength(&richData(w)->doc), true);
    +}
    +
    +
    +// The whole document from RTF (plain text when it is not RTF); the undo
    +// steps are forgotten
    +void wgtRichEditSetRtf(WidgetT *w, const char *rtf) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    docLoad(w, rtf, rtf ? (int32_t)strlen(rtf) : 0);
    +}
    +
    +
    +// The selection's paragraphs (with none, the caret's) aligned align
    +void wgtRichEditSetSelAlign(WidgetT *w, int32_t align) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichEditDataT *ed = richData(w);
    +    RichUndoT      u;
    +    int32_t        a;
    +    int32_t        b;
    +
    +    if (w->readOnly || align < FLOW_ALIGN_LEFT || align > FLOW_ALIGN_RIGHT) {
    +        return;
    +    }
    +
    +    selRange(ed, &a, &b);
    +
    +    int32_t first = paraAt(ed, a);
    +    int32_t last  = paraAt(ed, b);
    +
    +    memset(&u, 0, sizeof(u));
    +    richTextInit(&u.was);
    +    u.para   = first;
    +    u.anchor = ed->anchor;
    +    u.caret  = ed->caret;
    +
    +    for (int32_t p = first; p <= last; p++) {
    +        arrput(u.aligns, richTextAlign(&ed->doc, p));
    +        dvxFlowDestroy(ed->paras[p].flow);
    +        ed->paras[p].flow = NULL;
    +    }
    +
    +    undoPush(&ed->undo, &u);
    +    undoClear(&ed->redo);
    +    richTextSetAlign(&ed->doc, first, last, align);
    +    docSync(w);
    +    dirtyRows(ed, ed->paras[first].y, ed->paras[last].y + ed->paras[last].height);
    +    refresh(w);
    +    widgetFireChange(w);
    +}
    +
    +
    +void wgtRichEditSetSelColor(WidgetT *w, uint32_t rgb) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichChangeT change;
    +
    +    memset(&change, 0, sizeof(change));
    +    change.set      = RICH_SET_COLOR;
    +    change.to.color = rgb;
    +    applyChange(w, &change);
    +}
    +
    +
    +// The selection's family (NULL: left alone; "": the control's) and size
    +// (0: left alone)
    +void wgtRichEditSetSelFont(WidgetT *w, const char *family, int32_t size) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichChangeT change;
    +
    +    memset(&change, 0, sizeof(change));
    +
    +    if (family) {
    +        change.set |= RICH_SET_FAMILY;
    +        snprintf(change.to.family, sizeof(change.to.family), "%s", family);
    +
    +        // The control's own family is the default
    +        if (strcasecmp(change.to.family, dvxFontFamilyOf(wgtFont(w))) == 0) {
    +            change.to.family[0] = '\0';
    +        }
    +    }
    +
    +    if (size > 0) {
    +        change.set     |= RICH_SET_SIZE;
    +        change.to.size  = (size == wgtFont(w)->pixelSize) ? 0 : size;
    +    }
    +
    +    applyChange(w, &change);
    +}
    +
    +
    +void wgtRichEditSetSelLength(WidgetT *w, int32_t len) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    int32_t start = wgtRichEditGetSelStart(w);
    +
    +    moveTo(w, start, false);
    +    moveTo(w, start + DVX_MAX(len, 0), true);
    +}
    +
    +
    +// RTF over the selection
    +void wgtRichEditSetSelRtf(WidgetT *w, const char *rtf) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichEditDataT *ed = richData(w);
    +    RichTextT      piece;
    +    int32_t        a;
    +    int32_t        b;
    +
    +    if (w->readOnly) {
    +        return;
    +    }
    +
    +    richTextInit(&piece);
    +
    +    if (richRtfRead(&piece, rtf, rtf ? (int32_t)strlen(rtf) : 0)) {
    +        selRange(ed, &a, &b);
    +        editReplace(w, a, b, &piece, false);
    +    } else {
    +        insertPlain(w, rtf ? rtf : "", rtf ? (int32_t)strlen(rtf) : 0, false);
    +    }
    +
    +    richTextFree(&piece);
    +}
    +
    +
    +void wgtRichEditSetSelStart(WidgetT *w, int32_t start) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    moveTo(w, start, false);
    +}
    +
    +
    +// The style bits in mask made style's on the selection (or typing)
    +void wgtRichEditSetSelStyle(WidgetT *w, uint32_t style, uint32_t mask) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichChangeT change;
    +
    +    memset(&change, 0, sizeof(change));
    +    change.styleMask = mask & (FONT_STYLE_FACE | FONT_STYLE_LINES);
    +    change.to.style  = style;
    +    applyChange(w, &change);
    +}
    +
    +
    +void wgtRichEditSetSelText(WidgetT *w, const char *text) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    insertPlain(w, text ? text : "", text ? (int32_t)strlen(text) : 0, false);
    +}
    +
    +
    +void wgtRichEditUndo(WidgetT *w) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichEditDataT *ed = richData(w);
    +
    +    undoApply(w, &ed->undo, &ed->redo);
    +}
    +
    +
    +void widgetRichEditCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    +    w->calcMinW = font->charWidth * RICH_MIN_COLS + (RICH_BORDER + RICH_PAD) * 2 + WGT_SB_W;
    +    w->calcMinH = font->charHeight * RICH_MIN_ROWS + (RICH_BORDER + RICH_PAD) * 2;
    +}
    +
    +
    +// Another text control took the selection: this one's goes
    +bool widgetRichEditClearSelection(WidgetT *w) {
    +    RichEditDataT *ed = richData(w);
    +
    +    if (ed->anchor == ed->caret) {
    +        return false;
    +    }
    +
    +    dirtyOffsets(w, ed->anchor, ed->caret);
    +    ed->anchor = ed->caret;
    +    return true;
    +}
    +
    +
    +void widgetRichEditDestroy(WidgetT *w) {
    +    RichEditDataT *ed = richData(w);
    +
    +    for (int32_t p = 0; p < arrlen(ed->paras); p++) {
    +        dvxFlowDestroy(ed->paras[p].flow);
    +    }
    +
    +    fontsClose(ed);
    +    arrfree(ed->fonts);
    +    arrfree(ed->paras);
    +    undoClear(&ed->undo);
    +    undoClear(&ed->redo);
    +    arrfree(ed->undo);
    +    arrfree(ed->redo);
    +    arrfree(ed->textOut);
    +    free(ed->rtfOut);
    +    richTextFree(&ed->doc);
    +    free(ed);
    +    w->data = NULL;
    +
    +    if (--sLiveCount == 0 && sClipValid) {
    +        richTextFree(&sClip);
    +        arrfree(sClipText);
    +        sClipValid = false;
    +    }
    +}
    +
    +
    +// The plain text, a line per paragraph (valid until the next call)
    +const char *widgetRichEditGetText(const WidgetT *w) {
    +    RichEditDataT *ed  = richData(w);
    +    int32_t        len = richTextLength(&ed->doc);
    +
    +    arrsetlen(ed->textOut, len + 1);
    +
    +    if (len > 0) {
    +        memcpy(ed->textOut, ed->doc.text, (size_t)len);
    +    }
    +
    +    ed->textOut[len] = '\0';
    +    return ed->textOut;
    +}
    +
    +
    +void widgetRichEditOnBlur(WidgetT *w) {
    +    RichEditDataT *ed = richData(w);
    +
    +    dirtyOffsets(w, ed->caret, ed->caret);
    +}
    +
    +
    +void widgetRichEditOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
    +    RichEditDataT *ed = richData(w);
    +
    +    (void)root;
    +    (void)x;
    +    (void)y;
    +    ed->sbDragging = false;
    +    ed->selecting  = false;
    +}
    +
    +
    +// A drag: the scroll bar's thumb, or the selection (scrolling a line when
    +// the mouse is past the text's top or bottom)
    +void widgetRichEditOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
    +    RichEditDataT *ed = richData(w);
    +    RichGeomT      g;
    +
    +    (void)root;
    +    geomOf(w, &g);
    +
    +    if (ed->sbDragging) {
    +        int32_t to   = widgetTextScrollbarDragToScroll(y, g.sbY, WGT_SB_W, g.sbH, ed->docH, g.textH, ed->sbDragOff);
    +
    +        if (to != ed->scrollY) {
    +            ed->scrollY   = to;
    +            ed->fullPaint = true;
    +            refresh(w);
    +        }
    +
    +        return;
    +    }
    +
    +    if (ed->selecting) {
    +        moveTo(w, hitTest(w, x, y), true);
    +    }
    +}
    +
    +
    +void widgetRichEditOnFocus(WidgetT *w) {
    +    RichEditDataT *ed = richData(w);
    +
    +    ed->caretOn = true;
    +    ed->blinkAt = platformClock();
    +    dirtyOffsets(w, ed->caret, ed->caret);
    +}
    +
    +
    +void widgetRichEditOnKey(WidgetT *w, int32_t key, int32_t mod) {
    +    RichEditDataT *ed    = richData(w);
    +    bool           shift = (mod & KEY_MOD_SHIFT) != 0;
    +    bool           ctrl  = (mod & KEY_MOD_CTRL) != 0;
    +    int32_t        len   = richTextLength(&ed->doc);
    +    int32_t        a;
    +    int32_t        b;
    +    int32_t        x;
    +    int32_t        y;
    +    int32_t        h;
    +
    +    selRange(ed, &a, &b);
    +    docSync(w);
    +
    +    // Moving
    +    if (key == KEY_LEFT || key == KEY_RIGHT) {
    +        int32_t to = (key == KEY_LEFT) ? ed->caret - 1 : ed->caret + 1;
    +
    +        if (!shift && a != b) {
    +            to = (key == KEY_LEFT) ? a : b;
    +        }
    +
    +        ed->wantX = RICH_NONE;
    +        moveTo(w, to, shift);
    +        return;
    +    }
    +
    +    if (key == KEY_CTRL_LEFT || key == KEY_CTRL_RIGHT) {
    +        ed->wantX = RICH_NONE;
    +        moveTo(w, (key == KEY_CTRL_LEFT) ? wordBoundaryLeft(ed->doc.text, ed->caret) : wordBoundaryRight(ed->doc.text, len, ed->caret), shift);
    +        return;
    +    }
    +
    +    if (key == KEY_UP || key == KEY_DOWN || key == KEY_PGUP || key == KEY_PGDN) {
    +        RichGeomT g;
    +
    +        geomOf(w, &g);
    +        caretAt(w, ed->caret, &x, &y, &h);
    +        int32_t wantX  = (ed->wantX == RICH_NONE) ? x : ed->wantX;
    +        int32_t toY    = (key == KEY_UP) ? y - 1 : (key == KEY_DOWN) ? y + h : (key == KEY_PGUP) ? y - g.textH : y + g.textH;
    +        int32_t target = hitTest(w, g.textX + wantX, g.textY + DVX_MAX(0, DVX_MIN(toY, ed->docH - 1)));
    +
    +        // Above the first line or below the last: its start or end
    +        if (toY < 0) {
    +            target = 0;
    +        } else if (toY >= ed->docH) {
    +            target = len;
    +        }
    +
    +        moveTo(w, target, shift);
    +        ed->wantX = wantX;
    +        return;
    +    }
    +
    +    if (key == KEY_HOME || key == KEY_END) {
    +        int32_t          p    = paraAt(ed, ed->caret);
    +        const RichParaT *pr   = &ed->paras[p];
    +        int32_t          line = dvxFlowLineOf(pr->flow, ed->caret - pr->start);
    +        int32_t          to   = pr->start + dvxFlowLineStart(pr->flow, line);
    +
    +        if (key == KEY_END) {
    +            to = pr->start + dvxFlowLineStart(pr->flow, line + 1);
    +
    +            // A wrapped line ends before the space it wrapped at
    +            if (line + 1 < dvxFlowLineCount(pr->flow) && to > 0 && ed->doc.text[to - 1] == ' ') {
    +                to--;
    +            }
    +        }
    +
    +        ed->wantX = RICH_NONE;
    +        moveTo(w, to, shift);
    +        return;
    +    }
    +
    +    if (key == KEY_CTRL_HOME || key == KEY_CTRL_END) {
    +        ed->wantX = RICH_NONE;
    +        moveTo(w, (key == KEY_CTRL_HOME) ? 0 : len, shift);
    +        return;
    +    }
    +
    +    // Commands
    +    if (key == KEY_CTRL_A) {
    +        wgtRichEditSelectAll(w);
    +        return;
    +    }
    +
    +    if (key == KEY_CTRL_C) {
    +        wgtRichEditCopy(w);
    +        return;
    +    }
    +
    +    if (w->readOnly) {
    +        return;
    +    }
    +
    +    if (key == KEY_CTRL_X) {
    +        wgtRichEditCut(w);
    +    } else if (key == KEY_CTRL_V) {
    +        wgtRichEditPaste(w);
    +    } else if (key == KEY_CTRL_Z) {
    +        wgtRichEditUndo(w);
    +    } else if (key == RICH_KEY_REDO) {
    +        wgtRichEditRedo(w);
    +    } else if (key == RICH_KEY_BOLD || key == RICH_KEY_UNDERLINE || (key == RICH_KEY_ITALIC && ctrl)) {
    +        uint32_t bit = (key == RICH_KEY_BOLD) ? FONT_STYLE_BOLD : (key == RICH_KEY_UNDERLINE) ? FONT_STYLE_UNDERLINE : FONT_STYLE_ITALIC;
    +
    +        wgtRichEditSetSelStyle(w, (wgtRichEditGetSelStyle(w) & bit) ? 0 : bit, bit);
    +    } else if (key == RICH_KEY_LEFT || key == RICH_KEY_CENTER || key == RICH_KEY_RIGHT) {
    +        wgtRichEditSetSelAlign(w, (key == RICH_KEY_LEFT) ? FLOW_ALIGN_LEFT : (key == RICH_KEY_CENTER) ? FLOW_ALIGN_CENTER : FLOW_ALIGN_RIGHT);
    +    } else if (key == KEY_BACKSPACE || key == KEY_DELETE) {
    +        if (a == b) {
    +            a = (key == KEY_BACKSPACE) ? a - 1 : a;
    +            b = (key == KEY_DELETE) ? b + 1 : b;
    +        }
    +
    +        if (a >= 0 && b <= len && a < b) {
    +            editReplace(w, a, b, NULL, false);
    +        }
    +    } else if (key == KEY_ENTER) {
    +        insertPlain(w, "\n", 1, false);
    +    } else if (!(key & KEY_EXT_FLAG) && key >= KEY_ASCII_PRINT_FIRST && key <= UINT8_MAX && key != KEY_ASCII_DEL && !(mod & (KEY_MOD_CTRL | KEY_MOD_ALT))) {
    +        char c = (char)key;
    +
    +        insertPlain(w, &c, 1, true);
    +    }
    +}
    +
    +
    +// A click: the scroll bar, or the caret (Shift: the selection) to the
    +// point; a double click selects a word, a triple click the paragraph,
    +// and a drag selects
    +void widgetRichEditOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
    +    RichEditDataT *ed  = richData(w);
    +    AppContextT   *ctx = wgtGetContext(w);
    +    RichGeomT      g;
    +
    +    (void)root;
    +    sFocusedWidget = w;
    +    clearOtherSelections(w);
    +    docSync(w);
    +    geomOf(w, &g);
    +
    +    if (vx >= g.sbX) {
    +        int32_t old  = ed->scrollY;
    +        int32_t off  = 0;
    +
    +        if (widgetScrollbarApplyHit(g.sbH, vy - g.sbY, ed->docH, g.textH, wgtFont(w)->charHeight, g.textH, &ed->scrollY, &off)) {
    +            sDragWidget    = w;
    +            ed->sbDragging = true;
    +            ed->sbDragOff  = off;
    +        }
    +
    +        ed->fullPaint = ed->scrollY != old;
    +        refresh(w);
    +        return;
    +    }
    +
    +    int32_t at     = hitTest(w, vx, vy);
    +    int32_t clicks = multiClickDetect(vx, vy);
    +    bool    shift  = ctx && (ctx->keyModifiers & KEY_MOD_SHIFT);
    +    int32_t len    = richTextLength(&ed->doc);
    +
    +    if (clicks == 2) {
    +        moveTo(w, wordStart(ed->doc.text, at), false);
    +        moveTo(w, wordEnd(ed->doc.text, len, at), true);
    +        return;
    +    }
    +
    +    if (clicks >= 3) {
    +        int32_t p = paraAt(ed, at);
    +
    +        moveTo(w, ed->paras[p].start, false);
    +        moveTo(w, ed->paras[p].start + ed->paras[p].len, true);
    +        return;
    +    }
    +
    +    ed->wantX = RICH_NONE;
    +    moveTo(w, at, shift);
    +    sDragWidget   = w;
    +    ed->selecting = true;
    +}
    +
    +
    +bool widgetRichEditOnWheel(WidgetT *w, int32_t delta) {
    +    RichEditDataT *ed = richData(w);
    +    RichGeomT      g;
    +
    +    geomOf(w, &g);
    +    int32_t to   = ed->scrollY + delta * RICH_WHEEL_LINES * wgtFont(w)->charHeight;
    +
    +    to = DVX_MAX(0, DVX_MIN(to, DVX_MAX(ed->docH - g.textH, 0)));
    +
    +    if (to != ed->scrollY) {
    +        ed->scrollY   = to;
    +        ed->fullPaint = true;
    +        refresh(w);
    +    }
    +
    +    return true;
    +}
    +
    +
    +// The whole control: the sunken border, the text area, the scroll bar
    +void widgetRichEditPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
    +    RichEditDataT *ed = richData(w);
    +    uint32_t       bg = w->bgColor ? w->bgColor : colors->contentBg;
    +    RichGeomT      g;
    +
    +    (void)font;
    +    docSync(w);
    +    geomOf(w, &g);
    +    BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, RICH_BORDER);
    +
    +    ed->scrollY = DVX_MAX(0, DVX_MIN(ed->scrollY, DVX_MAX(ed->docH - g.textH, 0)));
    +    geomOf(w, &g);
    +    drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
    +    widgetDrawScrollbarV(d, ops, colors, g.sbX, g.sbY, g.sbH, ed->docH, g.textH, ed->scrollY);
    +
    +    int32_t cx = d->clipX;
    +    int32_t cy = d->clipY;
    +    int32_t cw = d->clipW;
    +    int32_t ch = d->clipH;
    +
    +    setClipRect(d, w->x + RICH_BORDER, g.viewY, g.sbX - w->x - RICH_BORDER, g.viewH);
    +    paintRows(w, d, ops, colors, ed->scrollY - RICH_PAD, ed->scrollY - RICH_PAD + g.viewH);
    +    setClipRect(d, cx, cy, cw, ch);
    +    ed->fullPaint   = false;
    +    ed->dirtyTop    = 0;
    +    ed->dirtyBottom = 0;
    +}
    +
    +
    +// The caret's blink while the control has the focus
    +void widgetRichEditPoll(WidgetT *w, WindowT *win) {
    +    RichEditDataT *ed  = richData(w);
    +    PlatformTicksT now = platformClock();
    +
    +    (void)win;
    +
    +    if (w != sFocusedWidget || (now - ed->blinkAt) < PLATFORM_MS_TO_TICKS(TEXT_CURSOR_BLINK_MS)) {
    +        return;
    +    }
    +
    +    ed->blinkAt = now;
    +    ed->caretOn = !ed->caretOn;
    +    dirtyOffsets(w, ed->caret, ed->caret);
    +}
    +
    +
    +// The rows an edit, a selection or the caret dirtied, painted straight
    +// into the window: their top and height in *outY / *outH, 0 for none
    +int32_t widgetRichEditQuickRepaint(WidgetT *w, int32_t *outY, int32_t *outH) {
    +    RichEditDataT *ed = richData(w);
    +    DisplayT       cd;
    +    RichGeomT      g;
    +
    +    if (ed->fullPaint) {
    +        refresh(w);
    +        return 0;
    +    }
    +
    +    if (ed->dirtyTop >= ed->dirtyBottom) {
    +        return 0;
    +    }
    +
    +    int32_t top    = ed->dirtyTop;
    +    int32_t bottom = ed->dirtyBottom;
    +
    +    ed->dirtyTop    = 0;
    +    ed->dirtyBottom = 0;
    +
    +    if (!widgetQuickPaintBegin(w, &cd)) {
    +        return 0;
    +    }
    +
    +    AppContextT *ctx = wgtGetContext(w);
    +
    +    geomOf(w, &g);
    +
    +    // The band on screen, inside the text area
    +    int32_t y0 = DVX_MAX(g.textY + top, g.viewY);
    +    int32_t y1 = DVX_MIN(g.textY + bottom, g.viewY + g.viewH);
    +    RectT   band;
    +    RectT   clip;
    +
    +    band.x = w->x + RICH_BORDER;
    +    band.y = y0;
    +    band.w = g.sbX - band.x;
    +    band.h = y1 - y0;
    +    clip.x = cd.clipX;
    +    clip.y = cd.clipY;
    +    clip.w = cd.clipW;
    +    clip.h = cd.clipH;
    +
    +    if (y1 <= y0 || !rectIntersect(&clip, &band, &clip)) {
    +        return 0;
    +    }
    +
    +    setClipRect(&cd, clip.x, clip.y, clip.w, clip.h);
    +    paintRows(w, &cd, &ctx->blitOps, &ctx->colors, y0 - g.textY, y1 - g.textY);
    +
    +    if (outY) {
    +        *outY = y0;
    +    }
    +
    +    if (outH) {
    +        *outH = y1 - y0;
    +    }
    +
    +    return 1;
    +}
    +
    +
    +// The plain text, a paragraph per line: the caret to the start, the undo
    +// steps forgotten
    +void widgetRichEditSetText(WidgetT *w, const char *text) {
    +    docFromPlain(richData(w), text ? text : "", text ? (int32_t)strlen(text) : 0);
    +    docStart(w);
    +}
    +
    +
    +// ============================================================
    +// DXE registration
    +// ============================================================
    +
    +
    +static const WidgetClassT sClassRichEdit = {
    +    .version = WGT_CLASS_VERSION,
    +    .flags   = WCLASS_FOCUSABLE | WCLASS_NEEDS_POLL,
    +    .handlers = {
    +        [WGT_METHOD_PAINT]           = (void *)widgetRichEditPaint,
    +        [WGT_METHOD_CALC_MIN_SIZE]   = (void *)widgetRichEditCalcMinSize,
    +        [WGT_METHOD_ON_MOUSE]        = (void *)widgetRichEditOnMouse,
    +        [WGT_METHOD_ON_KEY]          = (void *)widgetRichEditOnKey,
    +        [WGT_METHOD_DESTROY]         = (void *)widgetRichEditDestroy,
    +        [WGT_METHOD_GET_TEXT]        = (void *)widgetRichEditGetText,
    +        [WGT_METHOD_SET_TEXT]        = (void *)widgetRichEditSetText,
    +        [WGT_METHOD_CLEAR_SELECTION] = (void *)widgetRichEditClearSelection,
    +        [WGT_METHOD_ON_DRAG_UPDATE]  = (void *)widgetRichEditOnDragUpdate,
    +        [WGT_METHOD_ON_DRAG_END]     = (void *)widgetRichEditOnDragEnd,
    +        [WGT_METHOD_POLL]            = (void *)widgetRichEditPoll,
    +        [WGT_METHOD_QUICK_REPAINT]   = (void *)widgetRichEditQuickRepaint,
    +        [WGT_METHOD_ON_WHEEL]        = (void *)widgetRichEditOnWheel,
    +        [WGT_METHOD_ON_FOCUS]        = (void *)widgetRichEditOnFocus,
    +        [WGT_METHOD_ON_BLUR]         = (void *)widgetRichEditOnBlur,
    +    }
    +};
    +
    +static const RichEditApiT sApi = {
    +    .header         = WGT_API_HEADER(RichEditApiT),
    +    .create         = wgtRichEdit,
    +    .getRtf         = wgtRichEditGetRtf,
    +    .setRtf         = wgtRichEditSetRtf,
    +    .getSelRtf      = wgtRichEditGetSelRtf,
    +    .setSelRtf      = wgtRichEditSetSelRtf,
    +    .getSelStart    = wgtRichEditGetSelStart,
    +    .setSelStart    = wgtRichEditSetSelStart,
    +    .getSelLength   = wgtRichEditGetSelLength,
    +    .setSelLength   = wgtRichEditSetSelLength,
    +    .getSelText     = wgtRichEditGetSelText,
    +    .setSelText     = wgtRichEditSetSelText,
    +    .getSelFontName = wgtRichEditGetSelFontName,
    +    .getSelFontSize = wgtRichEditGetSelFontSize,
    +    .setSelFont     = wgtRichEditSetSelFont,
    +    .getSelStyle    = wgtRichEditGetSelStyle,
    +    .setSelStyle    = wgtRichEditSetSelStyle,
    +    .getSelColor    = wgtRichEditGetSelColor,
    +    .setSelColor    = wgtRichEditSetSelColor,
    +    .getSelAlign    = wgtRichEditGetSelAlign,
    +    .setSelAlign    = wgtRichEditSetSelAlign,
    +    .canUndo        = wgtRichEditCanUndo,
    +    .canRedo        = wgtRichEditCanRedo,
    +    .undo           = wgtRichEditUndo,
    +    .redo           = wgtRichEditRedo,
    +    .cut            = wgtRichEditCut,
    +    .copy           = wgtRichEditCopy,
    +    .paste          = wgtRichEditPaste,
    +    .selectAll      = wgtRichEditSelectAll,
    +    .find           = wgtRichEditFind,
    +    .loadFile       = wgtRichEditLoadFile,
    +    .saveFile       = wgtRichEditSaveFile
    +};
    +
    +static const WgtPropDescT sProps[] = {
    +    { "CanRedo",       WGT_IFACE_BOOL,   (void *)wgtRichEditCanRedo,        NULL,                             NULL },
    +    { "CanUndo",       WGT_IFACE_BOOL,   (void *)wgtRichEditCanUndo,        NULL,                             NULL },
    +    { "SelAlignment",  WGT_IFACE_INT,    (void *)wgtRichEditGetSelAlign,    (void *)wgtRichEditSetSelAlign,   NULL },
    +    { "SelBold",       WGT_IFACE_BOOL,   (void *)basGetSelBold,             (void *)basSetSelBold,            NULL },
    +    { "SelColor",      WGT_IFACE_INT,    (void *)basGetSelColor,            (void *)basSetSelColor,           NULL },
    +    { "SelFontName",   WGT_IFACE_STRING, (void *)wgtRichEditGetSelFontName, (void *)basSetSelFontName,        NULL },
    +    { "SelFontSize",   WGT_IFACE_INT,    (void *)wgtRichEditGetSelFontSize, (void *)basSetSelFontSize,        NULL },
    +    { "SelItalic",     WGT_IFACE_BOOL,   (void *)basGetSelItalic,           (void *)basSetSelItalic,          NULL },
    +    { "SelLength",     WGT_IFACE_INT,    (void *)wgtRichEditGetSelLength,   (void *)wgtRichEditSetSelLength,  NULL },
    +    { "SelRTF",        WGT_IFACE_STRING, (void *)wgtRichEditGetSelRtf,      (void *)wgtRichEditSetSelRtf,     NULL },
    +    { "SelStart",      WGT_IFACE_INT,    (void *)wgtRichEditGetSelStart,    (void *)wgtRichEditSetSelStart,   NULL },
    +    { "SelStrikethru", WGT_IFACE_BOOL,   (void *)basGetSelStrike,           (void *)basSetSelStrike,          NULL },
    +    { "SelText",       WGT_IFACE_STRING, (void *)wgtRichEditGetSelText,     (void *)wgtRichEditSetSelText,    NULL },
    +    { "SelUnderline",  WGT_IFACE_BOOL,   (void *)basGetSelUnderline,        (void *)basSetSelUnderline,       NULL },
    +    { "TextRTF",       WGT_IFACE_STRING, (void *)wgtRichEditGetRtf,         (void *)wgtRichEditSetRtf,        NULL }
    +};
    +
    +static const WgtMethodDescT sMethods[] = {
    +    { "Copy",      WGT_SIG_VOID,        (void *)wgtRichEditCopy },
    +    { "Cut",       WGT_SIG_VOID,        (void *)wgtRichEditCut },
    +    { "Find",      WGT_SIG_RET_INT_STR, (void *)basFind },
    +    { "LoadFile",  WGT_SIG_RET_INT_STR, (void *)wgtRichEditLoadFile },
    +    { "Paste",     WGT_SIG_VOID,        (void *)wgtRichEditPaste },
    +    { "Redo",      WGT_SIG_VOID,        (void *)wgtRichEditRedo },
    +    { "SaveFile",  WGT_SIG_RET_INT_STR, (void *)wgtRichEditSaveFile },
    +    { "SelectAll", WGT_SIG_VOID,        (void *)wgtRichEditSelectAll },
    +    { "Undo",      WGT_SIG_VOID,        (void *)wgtRichEditUndo }
    +};
    +
    +static const WgtEventDescT sEvents[] = {
    +    { RICHEDIT_EVT_SELCHANGE, "" }
    +};
    +
    +static const WgtIfaceT sIface = {
    +    .basName      = "RichEdit",
    +    .props        = sProps,
    +    .propCount    = DVX_ARRAY_LEN(sProps),
    +    .methods      = sMethods,
    +    .methodCount  = DVX_ARRAY_LEN(sMethods),
    +    .events       = sEvents,
    +    .eventCount   = DVX_ARRAY_LEN(sEvents),
    +    .createSig    = WGT_CREATE_PARENT,
    +    .defaultEvent = "Change"
    +};
    +
    +void wgtRegister(void) {
    +    sTypeId = wgtRegisterClass(&sClassRichEdit);
    +    wgtRegisterApi("richedit", &sApi);
    +    wgtRegisterIface("richedit", &sIface);
    +    wgtIfaceSetType("richedit", sTypeId);
    +}
    diff --git a/src/widgets/kpunch/richLabel/richLbl.h b/src/widgets/kpunch/richLabel/richLbl.h
    new file mode 100644
    index 00000000..8474cddd
    --- /dev/null
    +++ b/src/widgets/kpunch/richLabel/richLbl.h
    @@ -0,0 +1,70 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +// richLbl.h  -- RichLabel widget API
    +//
    +// Text in fonts, styles, and colors of its own, built by calls: SetFont,
    +// SetStyle, and SetColor choose what the next AddText uses; AddLineBreak
    +// and AddParagraph shape it (dvxFlow.h).
    +#ifndef RICHLBL_H
    +#define RICHLBL_H
    +
    +#include "dvxWgt.h"
    +#include "dvxFlow.h"
    +
    +// SetColor's "the control's ForeColor"
    +#define RICHLABEL_COLOR_DEFAULT 0xFFFFFFFFu
    +
    +typedef struct {
    +    WgtApiHeaderT header;
    +    WidgetT *(*create)(WidgetT *parent);
    +    void     (*clear)(WidgetT *w);
    +    void     (*setFont)(WidgetT *w, const char *family, int32_t size);
    +    void     (*setStyle)(WidgetT *w, int32_t style);
    +    void     (*setColor)(WidgetT *w, uint32_t rgb);
    +    void     (*addText)(WidgetT *w, const char *text);
    +    void     (*addLineBreak)(WidgetT *w);
    +    void     (*addParagraph)(WidgetT *w, int32_t align);
    +} RichLabelApiT;
    +
    +// Consumer side: the accessor and convenience macros.  The widget's own
    +// implementation defines RICHLABEL_IMPL so its functions keep their names.
    +#ifndef RICHLABEL_IMPL
    +
    +static inline const RichLabelApiT *dvxRichLabelApi(void) {
    +    static const RichLabelApiT *sApi;
    +    if (!sApi) { sApi = (const RichLabelApiT *)wgtGetApi("richlabel", sizeof(RichLabelApiT)); }
    +    return sApi;
    +}
    +
    +#define wgtRichLabel(parent)                    dvxRichLabelApi()->create(parent)
    +#define wgtRichLabelClear(w)                    dvxRichLabelApi()->clear(w)
    +#define wgtRichLabelSetFont(w, family, size)    dvxRichLabelApi()->setFont(w, family, size)
    +#define wgtRichLabelSetStyle(w, style)          dvxRichLabelApi()->setStyle(w, style)
    +#define wgtRichLabelSetColor(w, rgb)            dvxRichLabelApi()->setColor(w, rgb)
    +#define wgtRichLabelAddText(w, text)            dvxRichLabelApi()->addText(w, text)
    +#define wgtRichLabelAddLineBreak(w)             dvxRichLabelApi()->addLineBreak(w)
    +#define wgtRichLabelAddParagraph(w, align)      dvxRichLabelApi()->addParagraph(w, align)
    +
    +#endif // RICHLABEL_IMPL
    +
    +#endif // RICHLBL_H
    diff --git a/src/widgets/kpunch/richLabel/richlbl.bhs b/src/widgets/kpunch/richLabel/richlbl.bhs
    new file mode 100644
    index 00000000..c51d8bb2
    --- /dev/null
    +++ b/src/widgets/kpunch/richLabel/richlbl.bhs
    @@ -0,0 +1,85 @@
    +# The MIT License (MIT)
    +#
    +# Copyright (C) 2026 Scott Duensing
    +#
    +# Permission is hereby granted, free of charge, to any person obtaining a copy
    +# of this software and associated documentation files (the "Software"), to
    +# deal in the Software without restriction, including without limitation the
    +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +# sell copies of the Software, and to permit persons to whom the Software is
    +# furnished to do so, subject to the following conditions:
    +#
    +# The above copyright notice and this permission notice shall be included in
    +# all copies or substantial portions of the Software.
    +#
    +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +# IN THE SOFTWARE.
    +
    +.topic ctrl.richlabel
    +.title RichLabel
    +.toc 1 RichLabel
    +.index RichLabel
    +.index AddText
    +.index SetFont
    +.index SetStyle
    +.index SetColor
    +.index AddParagraph
    +.index AddLineBreak
    +
    +.h1 RichLabel
    +
    +DVX Widget: richlabel
    +
    +Text that mixes fonts, sizes, bold and italic, underlining, and colors, in paragraphs that are each aligned left, centered, or right. Build the text with calls: SetFont, SetStyle, and SetColor choose how the text you add next looks; AddText adds it; AddLineBreak starts a new line; and AddParagraph starts a new paragraph. Clear empties the control so you can build it again.
    +
    +Text starts in the control's own font (FontName, FontSize, and the other Font properties, or the form's). Set Width to have the text wrap at that width; otherwise each paragraph stays on one line and the control is as wide as its longest line.
    +
    +.h2 Methods
    +
    +.table
    +  Method                      Description
    +  ------                      -----------
    +  Clear                       Remove all the text. The font, style, and color go back to the control's own.
    +  SetFont family, size        The font family (for example "DejaVu Serif") and size in pixels of the text added next. "" and 0 go back to the control's own font and size.
    +  SetStyle style              FONT_BOLD, FONT_ITALIC, FONT_UNDERLINE, and FONT_STRIKE added together (from font.bas; 0 for plain) for the text added next.
    +  SetColor color              The color of the text added next, as RGB() makes it; -1 for the control's ForeColor.
    +  AddText text                Add text in the font, style, and color chosen.
    +  AddLineBreak                Start a new line in the same paragraph.
    +  AddParagraph alignment      Start a new paragraph: 0 left, 1 centered, 2 right (as a Label's Alignment).
    +.endtable
    +
    +Changing the font, style, or color affects only the text added afterwards. A line is as tall as the largest font on it.
    +
    +No type-specific properties or events.
    +
    +Default Event: Click
    +
    +.h2 Example
    +
    +.code
    +Sub Form_Load ()
    +    RichLabel1.Clear
    +    RichLabel1.SetFont "DejaVu Serif", 18
    +    RichLabel1.SetStyle FONT_BOLD
    +    RichLabel1.AddText "Welcome"
    +    RichLabel1.AddParagraph 0
    +    RichLabel1.SetFont "", 0
    +    RichLabel1.SetStyle 0
    +    RichLabel1.AddText "Text in "
    +    RichLabel1.SetColor RGB(160, 0, 0)
    +    RichLabel1.AddText "red"
    +    RichLabel1.SetColor -1
    +    RichLabel1.AddText " and "
    +    RichLabel1.SetStyle FONT_ITALIC + FONT_UNDERLINE
    +    RichLabel1.AddText "italic underlined"
    +    RichLabel1.SetStyle 0
    +    RichLabel1.AddText " words."
    +End Sub
    +.endcode
    +
    +.link ctrl.common.props Common Properties, Events, and Methods
    diff --git a/src/widgets/kpunch/richLabel/richlbl.bmp b/src/widgets/kpunch/richLabel/richlbl.bmp
    new file mode 100644
    index 00000000..98e44597
    --- /dev/null
    +++ b/src/widgets/kpunch/richLabel/richlbl.bmp
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:dc379786fcd8779c9d61efe3f4613fac3eb00566ad2e81309183fc81dd8dea83
    +size 1782
    diff --git a/src/widgets/kpunch/richLabel/richlbl.dhs b/src/widgets/kpunch/richLabel/richlbl.dhs
    new file mode 100644
    index 00000000..88b9f29c
    --- /dev/null
    +++ b/src/widgets/kpunch/richLabel/richlbl.dhs
    @@ -0,0 +1,104 @@
    +# The MIT License (MIT)
    +#
    +# Copyright (C) 2026 Scott Duensing
    +#
    +# Permission is hereby granted, free of charge, to any person obtaining a copy
    +# of this software and associated documentation files (the "Software"), to
    +# deal in the Software without restriction, including without limitation the
    +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +# sell copies of the Software, and to permit persons to whom the Software is
    +# furnished to do so, subject to the following conditions:
    +#
    +# The above copyright notice and this permission notice shall be included in
    +# all copies or substantial portions of the Software.
    +#
    +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +# IN THE SOFTWARE.
    +.section Technical Reference (for Developers)|Widgets
    +.topic widget.richlabel
    +.title RichLabel
    +.toc 0 RichLabel
    +.index RichLabel
    +.index wgtRichLabel
    +.index wgtRichLabelAddText
    +.index wgtRichLabelSetFont
    +
    +.h2 RichLabel
    +
    +Text in many fonts, styles, and colors, wrapped and aligned paragraph by paragraph. The text is built by calls: wgtRichLabelSetFont, wgtRichLabelSetStyle, and wgtRichLabelSetColor choose what the following wgtRichLabelAddText uses; wgtRichLabelAddLineBreak starts a new line and wgtRichLabelAddParagraph a new paragraph with its own alignment. Text starts in the widget's own font (wgtSetFont, or its container's). The widget is laid out at its width hint (prefW, else minW, which BASIC's Width sets), wrapping at spaces (a word too wide for a line breaks inside it); without one each paragraph is one line. Every line is as tall as the tallest font on it, its text standing on one baseline. Not focusable; drawn the same enabled or not. It fills its rectangle as a Label does: with the nearest explicit bgColor, else the content background. Built on the text flow in libdvx (dvxFlow.h).
    +
    +Header: widgets/richLbl.h
    +
    +.h3 Creation
    +
    +.code
    +WidgetT *rl = wgtRichLabel(parent);
    +
    +wgtRichLabelSetFont(rl, "DejaVu Serif", 18);
    +wgtRichLabelSetStyle(rl, FONT_STYLE_BOLD);
    +wgtRichLabelAddText(rl, "Chapter One");
    +wgtRichLabelAddParagraph(rl, FLOW_ALIGN_LEFT);
    +wgtRichLabelSetFont(rl, "", 0);
    +wgtRichLabelSetStyle(rl, 0);
    +wgtRichLabelAddText(rl, "It was a ");
    +wgtRichLabelSetColor(rl, DVX_RGB(160, 0, 0));
    +wgtRichLabelAddText(rl, "dark");
    +wgtRichLabelSetColor(rl, RICHLABEL_COLOR_DEFAULT);
    +wgtRichLabelAddText(rl, " and stormy night.");
    +.endcode
    +
    +.h3 API Functions
    +
    +.table
    +  Function                                       Description
    +  --------                                       -----------
    +  WidgetT *wgtRichLabel(parent)                  Create an empty rich label.
    +  void wgtRichLabelClear(w)                      Remove all text; the font, style, and color chosen go back to the widget's.
    +  void wgtRichLabelSetFont(w, family, size)      The font family and pixel size of the text added next ("" and 0: the widget's own).
    +  void wgtRichLabelSetStyle(w, style)            FONT_STYLE_BOLD, ITALIC, UNDERLINE, and STRIKE bits for the text added next.
    +  void wgtRichLabelSetColor(w, rgb)              0x00RRGGBB color of the text added next; RICHLABEL_COLOR_DEFAULT: the widget's fgColor (else the scheme's).
    +  void wgtRichLabelAddText(w, text)              Add text in the font, style, and color chosen.
    +  void wgtRichLabelAddLineBreak(w)               Start a new line in the same paragraph.
    +  void wgtRichLabelAddParagraph(w, align)        Start a new paragraph aligned FLOW_ALIGN_LEFT (0), FLOW_ALIGN_CENTER (1), or FLOW_ALIGN_RIGHT (2), as a Label's alignment; others are ignored.
    +.endtable
    +
    +Setting the font, style, or color changes only the text added afterwards.
    +
    +.h3 API Struct (wgtRegisterApi "richlabel")
    +
    +.table
    +  Slot           Function
    +  ----           --------
    +  header         WGT_API_HEADER (ABI version and size, checked by wgtGetApi)
    +  create         wgtRichLabel
    +  clear          wgtRichLabelClear
    +  setFont        wgtRichLabelSetFont
    +  setStyle       wgtRichLabelSetStyle
    +  setColor       wgtRichLabelSetColor
    +  addText        wgtRichLabelAddText
    +  addLineBreak   wgtRichLabelAddLineBreak
    +  addParagraph   wgtRichLabelAddParagraph
    +.endtable
    +
    +.h3 Methods (BASIC Interface)
    +
    +.table
    +  Method         Arguments              Description
    +  ------         ---------              -----------
    +  Clear          (none)                 wgtRichLabelClear
    +  SetFont        family, size           wgtRichLabelSetFont
    +  SetStyle       style                  wgtRichLabelSetStyle
    +  SetColor       color                  A BASIC color (RGB()); -1 for ForeColor
    +  AddText        text                   wgtRichLabelAddText
    +  AddLineBreak   (none)                 wgtRichLabelAddLineBreak
    +  AddParagraph   alignment              wgtRichLabelAddParagraph
    +.endtable
    +
    +.h3 Default Event
    +
    +"Click" (VB basName: RichLabel).
    diff --git a/src/widgets/kpunch/richLabel/richlbl.res b/src/widgets/kpunch/richLabel/richlbl.res
    new file mode 100644
    index 00000000..ea71c49c
    --- /dev/null
    +++ b/src/widgets/kpunch/richLabel/richlbl.res
    @@ -0,0 +1,7 @@
    +icon24      icon  richlbl.bmp
    +name        text  "RichLabel"
    +author      text  "Scott Duensing"
    +copyright   text  "Copyright 2026 Scott Duensing"
    +publisher   text  "Kangaroo Punch Studios"
    +description text  "Text in many fonts, styles, and colors"
    +version     text  "1.0"
    diff --git a/src/widgets/kpunch/richLabel/widgetRichLabel.c b/src/widgets/kpunch/richLabel/widgetRichLabel.c
    new file mode 100644
    index 00000000..6c356ce3
    --- /dev/null
    +++ b/src/widgets/kpunch/richLabel/widgetRichLabel.c
    @@ -0,0 +1,305 @@
    +// The MIT License (MIT)
    +//
    +// Copyright (C) 2026 Scott Duensing
    +//
    +// Permission is hereby granted, free of charge, to any person obtaining a copy
    +// of this software and associated documentation files (the "Software"), to
    +// deal in the Software without restriction, including without limitation the
    +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +// sell copies of the Software, and to permit persons to whom the Software is
    +// furnished to do so, subject to the following conditions:
    +//
    +// The above copyright notice and this permission notice shall be included in
    +// all copies or substantial portions of the Software.
    +//
    +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +// IN THE SOFTWARE.
    +
    +#define RICHLABEL_IMPL
    +// widgetRichLabel.c  -- RichLabel widget
    +//
    +// Text in many fonts, styles and colors, wrapped and aligned paragraph by
    +// paragraph: a text flow (dvxFlow.h) built by calls.  SetFont, SetStyle
    +// and SetColor choose what the following AddText uses; AddLineBreak starts
    +// a line and AddParagraph a paragraph with its alignment; Clear empties
    +// it.  Text starts in the control's own font (FontName and the rest, or
    +// its container's); SetFont "" goes back to it, a size of 0 keeps the
    +// control's size.  It wraps at its width hint (prefW, else minW: BASIC's
    +// Width); without one each paragraph is one line.  Not focusable, not
    +// interactive, drawn the same enabled or not (its runs keep their
    +// colors).  Paint fills the background as a Label does (the nearest
    +// explicit BackColor, else the content background).
    +
    +#include "dvxWgtP.h"
    +#include "dvxFlow.h"
    +#include "dvxText.h"
    +#include "richLbl.h"
    +
    +#include 
    +#include 
    +
    +// ABI stamp checked by the loader (see DVX_ABI_VERSION).
    +DVX_ABI_STAMP(richlbl);
    +
    +#define BAS_COLOR_DEFAULT (-1)          // SetColor in BASIC: the control's ForeColor
    +
    +static int32_t sTypeId = -1;
    +
    +typedef struct {
    +    TextFlowT *flow;
    +    char       family[FONT_FAMILY_MAX];  // "" = the control's font's
    +    int32_t    size;                     // 0 = the control's font's
    +    int32_t    style;                    // FONT_STYLE_*
    +    uint32_t   color;                    // 0x00RRGGBB or RICHLABEL_COLOR_DEFAULT
    +} RichLabelDataT;
    +
    +
    +// ============================================================
    +// Prototypes
    +// ============================================================
    +
    +static void     basSetColor(WidgetT *w, int32_t basicColor);
    +static uint32_t richBgColor(const WidgetT *w, const ColorSchemeT *colors);
    +WidgetT        *wgtRichLabel(WidgetT *parent);
    +void            wgtRichLabelAddLineBreak(WidgetT *w);
    +void            wgtRichLabelAddParagraph(WidgetT *w, int32_t align);
    +void            wgtRichLabelAddText(WidgetT *w, const char *text);
    +void            wgtRichLabelClear(WidgetT *w);
    +void            wgtRichLabelSetColor(WidgetT *w, uint32_t rgb);
    +void            wgtRichLabelSetFont(WidgetT *w, const char *family, int32_t size);
    +void            wgtRichLabelSetStyle(WidgetT *w, int32_t style);
    +void            widgetRichLabelCalcMinSize(WidgetT *w, const BitmapFontT *font);
    +void            widgetRichLabelDestroy(WidgetT *w);
    +void            widgetRichLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
    +static int32_t  wrapWidth(const WidgetT *w, const BitmapFontT *font);
    +void            wgtRegister(void);
    +
    +
    +// SetColor from BASIC: a BASIC color (&HBBGGRR), -1 for the ForeColor
    +static void basSetColor(WidgetT *w, int32_t basicColor) {
    +    wgtRichLabelSetColor(w, (basicColor == BAS_COLOR_DEFAULT) ? RICHLABEL_COLOR_DEFAULT : wgtBasicColor((uint32_t)basicColor));
    +}
    +
    +
    +// The nearest explicit background up from w, else the content background
    +static uint32_t richBgColor(const WidgetT *w, const ColorSchemeT *colors) {
    +    for (const WidgetT *p = w; p; p = p->parent) {
    +        if (p->bgColor) {
    +            return p->bgColor;
    +        }
    +    }
    +
    +    return colors->contentBg;
    +}
    +
    +
    +WidgetT *wgtRichLabel(WidgetT *parent) {
    +    WidgetT *w = widgetAlloc(parent, sTypeId);
    +
    +    if (!w) {
    +        return NULL;
    +    }
    +
    +    RichLabelDataT *d = (RichLabelDataT *)calloc(1, sizeof(RichLabelDataT));
    +
    +    if (d) {
    +        d->flow = dvxFlowCreate();
    +    }
    +
    +    if (!d || !d->flow) {
    +        free(d);
    +        widgetAllocRollback(w);
    +        return NULL;
    +    }
    +
    +    d->color = RICHLABEL_COLOR_DEFAULT;
    +    w->data  = d;
    +    return w;
    +}
    +
    +
    +void wgtRichLabelAddLineBreak(WidgetT *w) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichLabelDataT *d = (RichLabelDataT *)w->data;
    +
    +    dvxFlowAddBreak(d->flow);
    +    wgtInvalidate(w);
    +}
    +
    +
    +// A new paragraph aligned 0 (left), 1 (center) or 2 (right), as Label's
    +// Alignment
    +void wgtRichLabelAddParagraph(WidgetT *w, int32_t align) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichLabelDataT *d = (RichLabelDataT *)w->data;
    +
    +    if (align < FLOW_ALIGN_LEFT || align > FLOW_ALIGN_RIGHT) {
    +        return;
    +    }
    +
    +    dvxFlowAddParagraph(d->flow, align);
    +    wgtInvalidate(w);
    +}
    +
    +
    +// text in the font, style and color chosen
    +void wgtRichLabelAddText(WidgetT *w, const char *text) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichLabelDataT    *d    = (RichLabelDataT *)w->data;
    +    const BitmapFontT *own  = wgtFont(w);
    +    BitmapFontT       *font = dvxFontOpen(d->family[0] ? d->family : dvxFontFamilyOf(own), d->size ? d->size : own->pixelSize, (uint32_t)d->style);
    +
    +    if (!text || !font) {
    +        dvxFontClose(font);
    +        return;
    +    }
    +
    +    dvxFlowAddText(d->flow, text, -1, font, d->color);
    +    dvxFontClose(font);
    +    wgtInvalidate(w);
    +}
    +
    +
    +// Empty it; the font, style and color chosen go back to the control's
    +void wgtRichLabelClear(WidgetT *w) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichLabelDataT *d = (RichLabelDataT *)w->data;
    +
    +    dvxFlowClear(d->flow);
    +    d->family[0] = '\0';
    +    d->size      = 0;
    +    d->style     = 0;
    +    d->color     = RICHLABEL_COLOR_DEFAULT;
    +    wgtInvalidate(w);
    +}
    +
    +
    +void wgtRichLabelSetColor(WidgetT *w, uint32_t rgb) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichLabelDataT *d = (RichLabelDataT *)w->data;
    +
    +    d->color = rgb;
    +}
    +
    +
    +void wgtRichLabelSetFont(WidgetT *w, const char *family, int32_t size) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichLabelDataT *d = (RichLabelDataT *)w->data;
    +
    +    snprintf(d->family, sizeof(d->family), "%s", family ? family : "");
    +    d->size = (size > 0) ? size : 0;
    +}
    +
    +
    +void wgtRichLabelSetStyle(WidgetT *w, int32_t style) {
    +    VALIDATE_WIDGET_VOID(w, sTypeId);
    +    RichLabelDataT *d = (RichLabelDataT *)w->data;
    +
    +    d->style = style & (FONT_STYLE_FACE | FONT_STYLE_LINES);
    +}
    +
    +
    +// Laid out at its wrap width (wrapWidth), else at its natural width;
    +// never shorter than a line of its own font
    +void widgetRichLabelCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    +    RichLabelDataT *d     = (RichLabelDataT *)w->data;
    +    int32_t         wrapW = wrapWidth(w, font);
    +
    +    w->calcMinH = DVX_MAX(dvxFlowLayout(d->flow, wrapW), font->charHeight);
    +    w->calcMinW = (wrapW == FLOW_WIDTH_NATURAL) ? dvxFlowWidth(d->flow) : wrapW;
    +}
    +
    +
    +void widgetRichLabelDestroy(WidgetT *w) {
    +    RichLabelDataT *d = (RichLabelDataT *)w->data;
    +
    +    dvxFlowDestroy(d->flow);
    +    free(d);
    +    w->data = NULL;
    +}
    +
    +
    +void widgetRichLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
    +    RichLabelDataT *rd    = (RichLabelDataT *)w->data;
    +    uint32_t        fg    = w->fgColor ? w->fgColor : colors->contentFg;
    +    int32_t         wrapW = wrapWidth(w, font);
    +
    +    // With a hint the text wraps where it was measured (the layout may
    +    // have stretched the widget wider); without one its paragraphs align
    +    // in the whole width
    +    rectFill(d, ops, w->x, w->y, w->w, w->h, richBgColor(w, colors));
    +    dvxFlowLayout(rd->flow, (wrapW == FLOW_WIDTH_NATURAL) ? w->w : DVX_MIN(wrapW, w->w));
    +    dvxFlowPaint(rd->flow, d, ops, w->x, w->y, fg);
    +}
    +
    +
    +// The width the text wraps at: its width hint (prefW, else minW: BASIC's
    +// Width sets minW), else FLOW_WIDTH_NATURAL
    +static int32_t wrapWidth(const WidgetT *w, const BitmapFontT *font) {
    +    int32_t hint  = w->prefW ? w->prefW : w->minW;
    +    int32_t wrapW = hint ? wgtResolveSize(hint, 0, font->charWidth) : 0;
    +
    +    return (wrapW > 0) ? wrapW : FLOW_WIDTH_NATURAL;
    +}
    +
    +
    +// ============================================================
    +// DXE registration
    +// ============================================================
    +
    +
    +static const WidgetClassT sClassRichLabel = {
    +    .version = WGT_CLASS_VERSION,
    +    .flags   = 0,
    +    .handlers = {
    +        [WGT_METHOD_PAINT]         = (void *)widgetRichLabelPaint,
    +        [WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetRichLabelCalcMinSize,
    +        [WGT_METHOD_DESTROY]       = (void *)widgetRichLabelDestroy,
    +    }
    +};
    +
    +static const RichLabelApiT sApi = {
    +    .header       = WGT_API_HEADER(RichLabelApiT),
    +    .create       = wgtRichLabel,
    +    .clear        = wgtRichLabelClear,
    +    .setFont      = wgtRichLabelSetFont,
    +    .setStyle     = wgtRichLabelSetStyle,
    +    .setColor     = wgtRichLabelSetColor,
    +    .addText      = wgtRichLabelAddText,
    +    .addLineBreak = wgtRichLabelAddLineBreak,
    +    .addParagraph = wgtRichLabelAddParagraph
    +};
    +
    +static const WgtMethodDescT sMethods[] = {
    +    { "AddLineBreak", WGT_SIG_VOID,    (void *)wgtRichLabelAddLineBreak },
    +    { "AddParagraph", WGT_SIG_INT,     (void *)wgtRichLabelAddParagraph },
    +    { "AddText",      WGT_SIG_STR,     (void *)wgtRichLabelAddText },
    +    { "Clear",        WGT_SIG_VOID,    (void *)wgtRichLabelClear },
    +    { "SetColor",     WGT_SIG_INT,     (void *)basSetColor },
    +    { "SetFont",      WGT_SIG_STR_INT, (void *)wgtRichLabelSetFont },
    +    { "SetStyle",     WGT_SIG_INT,     (void *)wgtRichLabelSetStyle }
    +};
    +
    +static const WgtIfaceT sIface = {
    +    .basName      = "RichLabel",
    +    .props        = NULL,
    +    .propCount    = 0,
    +    .methods      = sMethods,
    +    .methodCount  = DVX_ARRAY_LEN(sMethods),
    +    .events       = NULL,
    +    .eventCount   = 0,
    +    .createSig    = WGT_CREATE_PARENT,
    +    .defaultEvent = "Click"
    +};
    +
    +void wgtRegister(void) {
    +    sTypeId = wgtRegisterClass(&sClassRichLabel);
    +    wgtRegisterApi("richlabel", &sApi);
    +    wgtRegisterIface("richlabel", &sIface);
    +    wgtIfaceSetType("richlabel", sTypeId);
    +}
    diff --git a/src/widgets/kpunch/scrollPane/scrlpane.bhs b/src/widgets/kpunch/scrollPane/scrlpane.bhs
    index 73e9613b..71e1b4df 100644
    --- a/src/widgets/kpunch/scrollPane/scrlpane.bhs
    +++ b/src/widgets/kpunch/scrollPane/scrlpane.bhs
    @@ -30,7 +30,7 @@
     
     DVX Extension -- DVX Widget: scrollpane | Name Prefix: Scroll
     
    -A scrollable container. Place child controls inside and the ScrollPane automatically provides scrollbars when the content exceeds the visible area. Clicking on content that does not respond to the mouse (a Label, an Image or empty space) gives the ScrollPane the focus, so the arrow and page keys scroll it.
    +A scrollable container. Place child controls inside and the ScrollPane automatically provides scrollbars when the content exceeds the visible area. Clicking on content that does not respond to the mouse (a Label, an Image, or empty space) gives the ScrollPane the focus, so the arrow and page keys scroll it.
     
     .h2 Type-Specific Properties
     
    @@ -58,7 +58,7 @@ A scrollable container. Place child controls inside and the ScrollPane automatic
     .table
       Event    Description
       -----    -----------
    -  Change   The user scrolled the content (the scrollbars, dragging a scroll box, the arrow, page, Home and End keys); read ScrollTop and ScrollLeft for the new position. Setting ScrollTop or ScrollLeft, or ScrollToTop, does not fire it.
    +  Change   The user scrolled the content (the scrollbars, dragging a scroll box, the arrow, page, Home, and End keys); read ScrollTop and ScrollLeft for the new position. Setting ScrollTop or ScrollLeft, or ScrollToTop, does not fire it.
     .endtable
     
     .h2 Scrolling a Picture Bigger Than the Window
    diff --git a/src/widgets/kpunch/scrollPane/scrlpane.dhs b/src/widgets/kpunch/scrollPane/scrlpane.dhs
    index 81c1f7f1..3d25cf9b 100644
    --- a/src/widgets/kpunch/scrollPane/scrlpane.dhs
    +++ b/src/widgets/kpunch/scrollPane/scrlpane.dhs
    @@ -36,7 +36,7 @@
     
     .h2 ScrollPane
     
    -A scrollable container that provides vertical and/or horizontal scrollbars when its content exceeds the visible area. Place a single child (typically a VBox or HBox) inside the scroll pane. A click on content that does not handle the mouse itself (a label, an image or empty space) gives the scroll pane the keyboard focus, so the arrow and page keys scroll it.
    +A scrollable container that provides vertical and/or horizontal scrollbars when its content exceeds the visible area. Place a single child (typically a VBox or HBox) inside the scroll pane. A click on content that does not handle the mouse itself (a label, an image, or empty space) gives the scroll pane the keyboard focus, so the arrow and page keys scroll it.
     
     Header: widgets/scrlPane.h
     
    @@ -65,7 +65,7 @@ WidgetT *content = wgtVBox(sp);
     
     Padding and spacing are the common WidgetT padding / spacing fields (WGT_GAP_DEFAULT, the default, is none); wgtSetPaddingPx / wgtSetSpacingPx set them in pixels.
     
    -When the user moves the scroll position (scrollbar arrows and trough, dragging a thumb, the arrow, page, Home and End keys) the scroll pane fires onChange (widgetFireChange) after the position is clamped; the setters, wgtScrollPaneScrollToTop and scrolling a focused child into view do not. BASIC sees it as the ScrollPane's Change event.
    +When the user moves the scroll position (scrollbar arrows and trough, dragging a thumb, the arrow, page, Home, and End keys) the scroll pane fires onChange (widgetFireChange) after the position is clamped; the setters, wgtScrollPaneScrollToTop, and scrolling a focused child into view do not. BASIC sees it as the ScrollPane's Change event.
     
     .h3 API Struct (wgtRegisterApi "scrollpane")
     
    diff --git a/src/widgets/kpunch/scrollPane/widgetScrollPane.c b/src/widgets/kpunch/scrollPane/widgetScrollPane.c
    index 5382cbd2..18312f30 100644
    --- a/src/widgets/kpunch/scrollPane/widgetScrollPane.c
    +++ b/src/widgets/kpunch/scrollPane/widgetScrollPane.c
    @@ -400,14 +400,13 @@ int32_t wgtScrollPaneGetScrollTop(const WidgetT *w) {
     
     void wgtScrollPaneScrollToChild(WidgetT *w, const WidgetT *child) {
         VALIDATE_WIDGET_VOID(w, sTypeId);
    -    ScrollPaneDataT *sp  = (ScrollPaneDataT *)w->data;
    -    AppContextT     *ctx = wgtGetContext(w);
    +    ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data;
     
    -    if (!ctx || !child) {
    +    if (!child) {
             return;
         }
     
    -    SpGeomT g = spGeom(w, &ctx->font);
    +    SpGeomT g = spGeom(w, wgtFont(w));
     
         // Child's virtual offset within the scrollable content
         int32_t childOffY = child->y - w->y - spBorder(w) + sp->scrollPosV;
    @@ -513,17 +512,11 @@ void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font) {
     // Handle scrollbar thumb drag for vertical and horizontal scrollbars.
     static void widgetScrollPaneOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mouseX, int32_t mouseY) {
         (void)root;
    -    ScrollPaneDataT *sp  = (ScrollPaneDataT *)w->data;
    -    AppContextT     *ctx = wgtGetContext(w);
    -
    -    if (!ctx) {
    -        return;
    -    }
    -
    -    SpGeomT g    = spGeom(w, &ctx->font);
    -    int32_t org  = spBorder(w) + WGT_SB_W + sp->sbDragOff;
    -    int32_t oldV = sp->scrollPosV;
    -    int32_t oldH = sp->scrollPosH;
    +    ScrollPaneDataT *sp   = (ScrollPaneDataT *)w->data;
    +    SpGeomT          g    = spGeom(w, wgtFont(w));
    +    int32_t          org  = spBorder(w) + WGT_SB_W + sp->sbDragOff;
    +    int32_t          oldV = sp->scrollPosV;
    +    int32_t          oldH = sp->scrollPosH;
     
         if (sp->sbDragOrient == SP_ORIENT_V && g.maxScrollV > 0) {
             sp->scrollPosV = widgetScrollbarThumbDragScroll(g.innerH - WGT_SB_W * 2, g.contentMinH, g.innerH, mouseY - w->y - org, g.maxScrollV);
    @@ -543,14 +536,8 @@ static void widgetScrollPaneOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mous
     void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod) {
         (void)mod;
     
    -    ScrollPaneDataT *sp  = (ScrollPaneDataT *)w->data;
    -    AppContextT     *ctx = wgtGetContext(w);
    -
    -    if (!ctx) {
    -        return;
    -    }
    -
    -    const BitmapFontT *font = &ctx->font;
    +    ScrollPaneDataT   *sp   = (ScrollPaneDataT *)w->data;
    +    const BitmapFontT *font = wgtFont(w);
         SpGeomT            g    = spGeom(w, font);
         int32_t            oldV = sp->scrollPosV;
         int32_t            oldH = sp->scrollPosH;
    @@ -589,8 +576,7 @@ void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod) {
     // into scroll pane children since their coordinates may be outside
     // the pane's visible bounds.
     void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
    -    AppContextT       *ctx    = (AppContextT *)root->userData;
    -    const BitmapFontT *font   = &ctx->font;
    +    const BitmapFontT *font   = wgtFont(hit);
         SpGeomT            g      = spGeom(hit, font);
         int32_t            border = spBorder(hit);
         int32_t            innerX = hit->x + border;
    diff --git a/src/widgets/kpunch/slider/slider.dhs b/src/widgets/kpunch/slider/slider.dhs
    index 3f73db1f..4a63ccef 100644
    --- a/src/widgets/kpunch/slider/slider.dhs
    +++ b/src/widgets/kpunch/slider/slider.dhs
    @@ -81,7 +81,7 @@ No widget-specific methods.
     .table
       Callback     Description
       --------     -----------
    -  onChange     Fires when the slider value changes (not when a key, click or drag leaves it unchanged).
    +  onChange     Fires when the slider value changes (not when a key, click, or drag leaves it unchanged).
     .endtable
     
     .h3 Default Event
    diff --git a/src/widgets/kpunch/spinner/spinner.bhs b/src/widgets/kpunch/spinner/spinner.bhs
    index ceb4acb2..c49757b6 100644
    --- a/src/widgets/kpunch/spinner/spinner.bhs
    +++ b/src/widgets/kpunch/spinner/spinner.bhs
    @@ -36,6 +36,8 @@ DVX Extension -- DVX Widget: spinner | Name Prefix: Spin
     
     A numeric input with up/down buttons. Supports integer mode (default) and real-number mode with configurable decimal places. Up/Down arrows step by the step size, Page Up/Page Down by ten steps. Typed or pasted text is committed on Enter or when the control loses focus; Esc cancels the edit. Ctrl+Z undoes the last edit of the typed text; once the text is committed or reformatted there is nothing to undo. Reading Value while the user is typing returns the typed number without interrupting the edit. Change also fires as each character is typed; stepping at a limit, or committing text that parses to the unchanged value, does not fire it.
     
    +Its font must be fixed-pitch, every character the same width; the designer's Font dialog offers only those families for it. Given a proportional font it draws in the system font instead.
    +
     .h2 Type-Specific Properties
     
     .table
    diff --git a/src/widgets/kpunch/spinner/spinner.dhs b/src/widgets/kpunch/spinner/spinner.dhs
    index 58fd7627..3e493e23 100644
    --- a/src/widgets/kpunch/spinner/spinner.dhs
    +++ b/src/widgets/kpunch/spinner/spinner.dhs
    @@ -34,7 +34,9 @@
     
     A numeric input with up/down buttons for incrementing and decrementing a value within a range. Supports both integer and floating-point (real) modes.
     
    -Typing, Backspace/Delete, paste, cut and undo start an edit of the text; Enter or focus loss commits it and Esc cancels it. Undo (Ctrl+Z) covers edits of the typed text only: every reformat from the value (creation, SetText, commit, step) empties the undo slot. A paste that is not a valid number is rolled back.
    +Typing, Backspace/Delete, paste, cut, and undo start an edit of the text; Enter or focus loss commits it and Esc cancels it. Undo (Ctrl+Z) covers edits of the typed text only: every reformat from the value (creation, SetText, commit, step) empties the undo slot. A paste that is not a valid number is rolled back.
    +
    +WCLASS_FIXED_FONT: it edits by columns, so its font must be fixed-pitch. Any size works (the column math uses the font's charWidth and charHeight); a proportional font set with wgtSetFont, or inherited, is replaced by the system font when it draws.
     
     Header: widgets/spinner.h
     
    diff --git a/src/widgets/kpunch/spinner/widgetSpinner.c b/src/widgets/kpunch/spinner/widgetSpinner.c
    index 11c8384f..74f5f90c 100644
    --- a/src/widgets/kpunch/spinner/widgetSpinner.c
    +++ b/src/widgets/kpunch/spinner/widgetSpinner.c
    @@ -430,7 +430,7 @@ static bool spinnerValueDiffers(const SpinnerDataT *d, int32_t oldValue, double
     
     static const WidgetClassT sClassSpinner = {
         .version = WGT_CLASS_VERSION,
    -    .flags   = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE,
    +    .flags   = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE | WCLASS_FIXED_FONT,
         .handlers = {
             [WGT_METHOD_PAINT]         = (void *)widgetSpinnerPaint,
             [WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetSpinnerCalcMinSize,
    @@ -782,6 +782,7 @@ static void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod) {
     // fixed-width font. Double-click selects all text (select-word doesn't
     // make sense for numbers), entering edit mode to allow replacement.
     static void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
    +    (void)root;
         sFocusedWidget = hit;
         SpinnerDataT *d = (SpinnerDataT *)hit->data;
     
    @@ -793,9 +794,7 @@ static void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_
             return;
         }
     
    -    AppContextT *ctx = (AppContextT *)root->userData;
    -
    -    widgetTextEditMouseClick(hit, &d->te, vx, vy, hit->x + SPINNER_BORDER + TEXT_INPUT_PAD, &ctx->font, false, false);
    +    widgetTextEditMouseClick(hit, &d->te, vx, vy, hit->x + SPINNER_BORDER + TEXT_INPUT_PAD, wgtFont(hit), false, false);
         d->editing = true;
         wgtInvalidatePaint(hit);
     }
    diff --git a/src/widgets/kpunch/splitter/widgetSplitter.c b/src/widgets/kpunch/splitter/widgetSplitter.c
    index a8c9c0c3..dd62e44b 100644
    --- a/src/widgets/kpunch/splitter/widgetSplitter.c
    +++ b/src/widgets/kpunch/splitter/widgetSplitter.c
    @@ -381,8 +381,8 @@ void widgetSplitterLayout(WidgetT *w, const BitmapFontT *font) {
     // Moving the divider only changes this splitter's panes, so re-lay out
     // just this subtree and repaint it instead of relaying out the window.
     static void widgetSplitterOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mouseX, int32_t mouseY) {
    +    (void)root;
         SplitterDataT *d   = (SplitterDataT *)w->data;
    -    AppContextT   *ctx = (AppContextT *)root->userData;
         int32_t        pos = (d->vertical ? mouseX - w->x : mouseY - w->y) - d->dragStart;
     
         widgetSplitterClampPos(w, &pos);
    @@ -390,7 +390,7 @@ static void widgetSplitterOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mouseX
         if (pos != d->dividerPos) {
             d->dividerPos = pos;
             d->clearPanes = true;
    -        widgetSplitterLayout(w, &ctx->font);
    +        widgetSplitterLayout(w, wgtFont(w));
             wgtInvalidatePaint(w);
             widgetFireChange(w);
         }
    diff --git a/src/widgets/kpunch/tabControl/tabctrl.dhs b/src/widgets/kpunch/tabControl/tabctrl.dhs
    index 8ecd2a51..33959d05 100644
    --- a/src/widgets/kpunch/tabControl/tabctrl.dhs
    +++ b/src/widgets/kpunch/tabControl/tabctrl.dhs
    @@ -92,7 +92,7 @@ WidgetT *page2 = wgtTabPage(tabs, "Advanced");
     .table
       Callback     Description
       --------     -----------
    -  onChange     Fires when the user changes the active tab (mouse, keyboard or accelerator); not for wgtTabControlSetActive.
    +  onChange     Fires when the user changes the active tab (mouse, keyboard, or accelerator); not for wgtTabControlSetActive.
     .endtable
     
     .h3 Default Event
    diff --git a/src/widgets/kpunch/tabControl/widgetTabControl.c b/src/widgets/kpunch/tabControl/widgetTabControl.c
    index a40603a4..edd7eeed 100644
    --- a/src/widgets/kpunch/tabControl/widgetTabControl.c
    +++ b/src/widgets/kpunch/tabControl/widgetTabControl.c
    @@ -120,6 +120,7 @@ static void        basAddPage(WidgetT *w, const char *title);
     static void        tabClosePopup(const WidgetT *page);
     static int32_t     tabCount(const WidgetT *w);
     static void        tabEnsureVisible(WidgetT *w, const BitmapFontT *font, const TabMetricsT *m);
    +static int32_t     tabHeaderH(const BitmapFontT *font);
     static TabMetricsT tabMetrics(const WidgetT *w, const BitmapFontT *font);
     static WidgetT    *tabPageAt(const WidgetT *w, int32_t idx);
     static void        tabPaintHeaders(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
    @@ -222,12 +223,19 @@ static void tabEnsureVisible(WidgetT *w, const BitmapFontT *font, const TabMetri
     }
     
     
    +// Height of the header strip: a line of the font with TAB_PAD_V above and
    +// below it.
    +static int32_t tabHeaderH(const BitmapFontT *font) {
    +    return font->charHeight + TAB_PAD_V * 2;
    +}
    +
    +
     // Header geometry: the tabs fill the strip inside the TAB_BORDER insets;
     // when they overflow, arrow buttons take TAB_ARROW_W at each end.
     static TabMetricsT tabMetrics(const WidgetT *w, const BitmapFontT *font) {
         TabMetricsT m;
     
    -    m.tabH   = font->charHeight + TAB_PAD_V * 2;
    +    m.tabH   = tabHeaderH(font);
         m.totalW = 0;
     
         for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
    @@ -466,7 +474,7 @@ WidgetT *wgtTabPage(WidgetT *parent, const char *title) {
     // enough space for the largest page, preventing resize flicker when
     // switching tabs. Children are recursively measured.
     void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    -    int32_t tabH     = font->charHeight + TAB_PAD_V * 2;
    +    int32_t tabH     = tabHeaderH(font);
         int32_t maxPageW = 0;
         int32_t maxPageH = 0;
     
    @@ -561,10 +569,10 @@ void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod) {
     // on the content area go through normal child hit-testing. Scroll
     // arrow clicks adjust scrollOffset by TAB_SCROLL_CHARS characters.
     void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
    +    (void)root;
         sFocusedWidget = hit;
         TabControlDataT   *d    = (TabControlDataT *)hit->data;
    -    AppContextT       *ctx  = (AppContextT *)root->userData;
    -    const BitmapFontT *font = &ctx->font;
    +    const BitmapFontT *font = wgtFont(hit);
         TabMetricsT        m    = tabMetrics(hit, font);
     
         // Only handle clicks in the tab header area
    diff --git a/src/widgets/kpunch/textInput/textinpt.bhs b/src/widgets/kpunch/textInput/textinpt.bhs
    index 882f5d60..a918b527 100644
    --- a/src/widgets/kpunch/textInput/textinpt.bhs
    +++ b/src/widgets/kpunch/textInput/textinpt.bhs
    @@ -39,6 +39,8 @@ VB Equivalent: TextBox -- DVX Widget: textbox (single-line text input, 256 chars
     
     A single-line text input field. Supports data binding via DataSource and DataField properties.
     
    +Its font must be fixed-pitch, every character the same width; the designer's Font dialog offers only those families for it. Given a proportional font it draws in the system font instead.
    +
     .h2 Type-Specific Properties
     
     .table
    @@ -52,7 +54,7 @@ A single-line text input field. Supports data binding via DataSource and DataFie
       DataField      String   Column name for data binding.
       SelStart       Integer  Where the selection starts, or the cursor when nothing is selected (0 = before the first character).  Setting it moves the cursor there and selects nothing.
       SelLength      Integer  The number of characters selected (0 = none).  Setting it selects that many characters from SelStart.
    -  SelText        String   The selected text.  Setting it replaces the selection (or inserts at the cursor), leaves the cursor after the new text and fires Change; in a masked box it fills the places from the selection's start, as pasting does.
    +  SelText        String   The selected text.  Setting it replaces the selection (or inserts at the cursor), leaves the cursor after the new text, and fires Change; in a masked box it fills the places from the selection's start, as pasting does.
     .endtable
     
     .h2 Type-Specific Methods
    @@ -67,7 +69,7 @@ A single-line text input field. Supports data binding via DataSource and DataFie
       Undo        Take back the last change (Ctrl+Z); a second Undo puts it back.
     .endtable
     
    -The methods do what the keys do, so Cut, Paste and Undo do nothing in a read-only box.  An Edit menu works on whichever box has the focus with Me.ActiveControl (see Form).
    +The methods do what the keys do, so Cut, Paste, and Undo do nothing in a read-only box.  An Edit menu works on whichever box has the focus with Me.ActiveControl (see Form).
     
     Default Event: Change
     
    @@ -114,6 +116,8 @@ VB Equivalent: TextArea (DVX extension) -- DVX Widget: textarea (multi-line text
     
     A multi-line text editing area. This is a DVX extension with no direct VB3 equivalent (VB uses a TextBox with MultiLine=True). Supports line numbers, auto-indent, find/replace, and tab configuration.
     
    +Its font must be fixed-pitch, every character the same width; the designer's Font dialog offers only those families for it. Given a proportional font it draws in the system font instead.
    +
     .h2 Type-Specific Properties
     
     .table
    @@ -125,7 +129,7 @@ A multi-line text editing area. This is a DVX extension with no direct VB3 equiv
       LineCount    Integer   R     The number of lines (an empty text has one).
       SelStart     Integer   R/W   Where the selection starts, or the cursor when nothing is selected (0 = before the first character).  Setting it moves the cursor there and selects nothing.
       SelLength    Integer   R/W   The number of characters selected (0 = none).  Setting it selects that many characters from SelStart.
    -  SelText      String    R/W   The selected text.  Setting it replaces the selection (or inserts at the cursor when nothing is selected), leaves the cursor after the new text and fires Change; Undo takes it back.
    +  SelText      String    R/W   The selected text.  Setting it replaces the selection (or inserts at the cursor when nothing is selected), leaves the cursor after the new text, and fires Change; Undo takes it back.
       MaxLength    Integer   R/W   The most characters the area holds (0 = the default, 65536; at most 262144).  A smaller value cuts the text.
     .endtable
     
    @@ -160,7 +164,7 @@ A multi-line text editing area. This is a DVX extension with no direct VB3 equiv
       Undo                                            Take back the last change (Ctrl+Z); a second Undo puts it back.
     .endtable
     
    -Cut, Paste and Undo do nothing in a read-only area.
    +Cut, Paste, and Undo do nothing in a read-only area.
     
     .h2 Type-Specific Events
     
    diff --git a/src/widgets/kpunch/textInput/textinpt.dhs b/src/widgets/kpunch/textInput/textinpt.dhs
    index b670d085..2980bcd5 100644
    --- a/src/widgets/kpunch/textInput/textinpt.dhs
    +++ b/src/widgets/kpunch/textInput/textinpt.dhs
    @@ -34,7 +34,9 @@
     
     .h2 TextInput / TextArea
     
    -Single-line text input, password input, masked input, and multi-line text area with optional syntax colorization (built-in BASIC and help-source colorizers, the BASIC one shared with the DVX BASIC IDE), line numbers, find/replace, line markers and gutter clicks.
    +Single-line text input, password input, masked input, and multi-line text area with optional syntax colorization (built-in BASIC and help-source colorizers, the BASIC one shared with the DVX BASIC IDE), line numbers, find/replace, line markers, and gutter clicks.
    +
    +WCLASS_FIXED_FONT: it edits by columns, so its font must be fixed-pitch. Any size works (the column math uses the font's charWidth and charHeight); a proportional font set with wgtSetFont, or inherited, is replaced by the system font when it draws.
     
     Header: widgets/textInpt.h
     
    @@ -85,7 +87,7 @@ Header: widgets/textInpt.h
       bool wgtTextAreaReplaceSelection(w, needle, repl, caseSens, fwd)  If the current selection matches needle, replace it, then find the next occurrence in direction fwd. Returns true if anything was replaced or found.
       void wgtTextAreaSetLineMarker(w, line, gutterRgb, backRgb)  Mark 1-based line: a dot of gutterRgb (0xRRGGBB) in the line-number gutter and / or the line's background in backRgb; TEXTAREA_MARKER_NONE leaves a part out, both NONE removes the marker. Markers keep their line numbers when the text changes (the owner moves them). The DVX BASIC IDE marks breakpoints and the paused line this way.
       void wgtTextAreaClearLineMarkers(w)                         Remove every line marker.
    -  void wgtTextAreaSetSyntaxMode(w, mode)                      Use a built-in colorizer: "bas" / "basic" (BASIC: the compiler's keyword list, date literals by the lexer's shape rule, &H / &O and exponent numbers, REM and ' comments; the IDE's code editor uses it), "dhs" / "help" (help source) or "custom" (each painted line goes to the "Colorize" event through wgtFireEvent: args LineText and Colors, a string of one digit per character, "0" = TEXT_SYNTAX_DEFAULT; the host writes the TEXT_SYNTAX_* digits back into Colors; the BASIC Colorize event); NULL or "" turns colouring off.
    +  void wgtTextAreaSetSyntaxMode(w, mode)                      Use a built-in colorizer: "bas" / "basic" (BASIC: the compiler's keyword list, date literals by the lexer's shape rule, &H / &O and exponent numbers, REM and ' comments; the IDE's code editor uses it), "dhs" / "help" (help source), or "custom" (each painted line goes to the "Colorize" event through wgtFireEvent: args LineText and Colors, a string of one digit per character, "0" = TEXT_SYNTAX_DEFAULT; the host writes the TEXT_SYNTAX_* digits back into Colors; the BASIC Colorize event); NULL or "" turns colouring off.
       int32_t wgtTextAreaGetCursorLine(w)                         Get the current cursor line number.
       void wgtTextAreaSetGutterClickCallback(w, fn)               Set a callback for gutter clicks (e.g. for breakpoint toggling).
       int32_t wgtTextAreaGetWordAtCursor(w, buf, bufSize)         Copy the word under the cursor into buf. Returns its length.
    @@ -98,7 +100,7 @@ Header: widgets/textInpt.h
       void wgtTextInputSetSelStart(w, start)                      Cursor to offset start, nothing selected.
       void wgtTextInputSetSelLength(w, length)                    Select length characters from SelStart (clamped to the text).
       void wgtTextInputSetSelText(w, text)                        Replace the selection (or insert at the cursor) with text (texthelp textEditSingleReplaceSel; a masked box fills its slots); fires onChange.
    -  void wgtTextInputCut(w), wgtTextInputCopy(w), wgtTextInputPaste(w), wgtTextInputSelectAll(w), wgtTextInputUndo(w)   The box's Ctrl+X / C / V / A / Z keys, run through its own key handler (password, mask and read-only rules included).
    +  void wgtTextInputCut(w), wgtTextInputCopy(w), wgtTextInputPaste(w), wgtTextInputSelectAll(w), wgtTextInputUndo(w)   The box's Ctrl+X / C / V / A / Z keys, run through its own key handler (password, mask, and read-only rules included).
     .endtable
     
     .h3 API Struct (wgtRegisterApi "textinput")
    @@ -120,7 +122,7 @@ Text is managed via the generic "Text" property. TextBox exposes:
       SelText        String     Read/Write   wgtTextInputGetSelText / wgtTextInputSetSelText.
     .endtable
     
    -TextBox methods: Copy, Cut, Paste, SelectAll and Undo (wgtTextInputCopy ...).
    +TextBox methods: Copy, Cut, Paste, SelectAll, and Undo (wgtTextInputCopy ...).
     
     TextArea exposes:
     
    @@ -143,7 +145,7 @@ TextArea exposes:
       ------                                            -----------
       AppendText text$                                  Append text to the end of the buffer.
       ClearLineMarkers                                  wgtTextAreaClearLineMarkers.
    -  Copy / Cut / Paste / SelectAll / Undo             The Ctrl+C / X / V / A / Z commands (the texthelp textEditMulti* functions the key handler runs); Cut, Paste and Undo fire Change and do nothing when read-only.
    +  Copy / Cut / Paste / SelectAll / Undo             The Ctrl+C / X / V / A / Z commands (the texthelp textEditMulti* functions the key handler runs); Cut, Paste, and Undo fire Change and do nothing when read-only.
       CursorToEnd                                       wgtTextAreaCursorToEnd.
       FindNext needle$, caseSensitive, forward          Search from the cursor (a match at the cursor counts; repeating moves past it). Returns True if found.
       GetLine(line%)                                    Returns a line's text (1-based), "" past the end.
    diff --git a/src/widgets/kpunch/textInput/widgetTextInput.c b/src/widgets/kpunch/textInput/widgetTextInput.c
    index fc6e4b74..0a14ad86 100644
    --- a/src/widgets/kpunch/textInput/widgetTextInput.c
    +++ b/src/widgets/kpunch/textInput/widgetTextInput.c
    @@ -140,9 +140,10 @@ typedef struct {
         char    *expandBuf;  // tab-expanded text (MAX_COLORIZE_LEN)
         uint8_t *syntaxBuf;  // tab-expanded syntax colors (MAX_COLORIZE_LEN)
     
    -    // Cached gutter width (recomputed only when line count changes)
    -    int32_t  cachedGutterW;
    -    int32_t  cachedGutterLines; // line count when cachedGutterW was computed
    +    // Cached gutter width in columns (recomputed only when line count
    +    // changes; columns, not pixels, so it holds across font changes)
    +    int32_t  cachedGutterCols;
    +    int32_t  cachedGutterLines; // line count when cachedGutterCols was computed
     } TextAreaDataT;
     
     // One entry of the built-in BASIC colorizer's keyword table.
    @@ -271,7 +272,6 @@ static uint32_t    textAreaPackRgb(const DisplayT *d, uint32_t rgb);
     static void        textAreaScrollToCursor(WidgetT *w);
     static bool        textAreaSelRange(const TextAreaDataT *ta, int32_t *pLo, int32_t *pHi);
     static void        textInputEdited(WidgetT *w, bool changed);
    -static int32_t     textInputVisibleChars(const WidgetT *w);
     static const char *textOut(const char *src, int32_t len);
     WidgetT           *wgtMaskedInput(WidgetT *parent, const char *mask);
     WidgetT           *wgtPasswordInput(WidgetT *parent, int32_t maxLen);
    @@ -954,7 +954,7 @@ static void textAreaComputeGeom(WidgetT *w, const BitmapFontT *font, TextAreaGeo
     static inline void textAreaDirtyCache(WidgetT *w) {
         TextAreaDataT *ta = (TextAreaDataT *)w->data;
         textEditLineCacheDirty(&ta->te.lines);
    -    ta->cachedGutterW = 0;
    +    ta->cachedGutterCols = 0;
     }
     
     
    @@ -996,8 +996,8 @@ static int32_t textAreaGutterWidth(WidgetT *w, const BitmapFontT *font) {
     
         int32_t totalLines = textAreaGetLineCount(w);
     
    -    if (ta->cachedGutterW > 0 && ta->cachedGutterLines == totalLines) {
    -        return ta->cachedGutterW;
    +    if (ta->cachedGutterCols > 0 && ta->cachedGutterLines == totalLines) {
    +        return ta->cachedGutterCols * font->charWidth;
         }
     
         int32_t digits = 1;
    @@ -1012,10 +1012,10 @@ static int32_t textAreaGutterWidth(WidgetT *w, const BitmapFontT *font) {
             digits = 3;
         }
     
    -    ta->cachedGutterW     = (digits + 1) * font->charWidth;
    +    ta->cachedGutterCols  = digits + 1;
         ta->cachedGutterLines = totalLines;
     
    -    return ta->cachedGutterW;
    +    return ta->cachedGutterCols * font->charWidth;
     }
     
     
    @@ -1071,16 +1071,15 @@ static uint32_t textAreaPackRgb(const DisplayT *d, uint32_t rgb) {
     
     // Scrolls the view so the cursor is visible under the current geometry.
     static void textAreaScrollToCursor(WidgetT *w) {
    -    TextAreaDataT *ta  = (TextAreaDataT *)w->data;
    -    AppContextT   *ctx = wgtGetContext(w);
    +    TextAreaDataT *ta = (TextAreaDataT *)w->data;
     
    -    if (!ctx) {
    +    if (!wgtGetContext(w)) {
             return;
         }
     
         TextAreaGeomT geom;
     
    -    textAreaComputeGeom(w, &ctx->font, &geom);
    +    textAreaComputeGeom(w, wgtFont(w), &geom);
         textEditEnsureVisible(&ta->te.lines, ta->te.buf, ta->te.len, ta->te.cursorRow, ta->te.cursorCol, ta->tabWidth, DVX_MAX(geom.visRows, 1), DVX_MAX(geom.visCols, 1), &ta->te.scrollRow, &ta->te.scrollCol);
     }
     
    @@ -1111,17 +1110,11 @@ static void textInputEdited(WidgetT *w, bool changed) {
     
         TextInputDataT *ti = (TextInputDataT *)w->data;
     
    -    textEditScrollToCursor(ti->te.cursorPos, textInputVisibleChars(w), &ti->te.scrollOff);
    +    textEditScrollToCursor(ti->te.cursorPos, textEditVisibleChars(w->w, wgtFont(w)), &ti->te.scrollOff);
         wgtInvalidatePaint(w);
     }
     
     
    -// Visible character columns of a single-line input (inside the pad).
    -static int32_t textInputVisibleChars(const WidgetT *w) {
    -    return textEditVisibleChars(w->w, &wgtGetContext(w)->font);
    -}
    -
    -
     // A string result for BASIC: len bytes of src, valid until the next call.
     static const char *textOut(const char *src, int32_t len) {
         static char *sOut = NULL;   // stb_ds
    @@ -1409,13 +1402,12 @@ void wgtTextAreaGoToLine(WidgetT *w, int32_t line) {
         }
     
         TextAreaDataT *ta      = (TextAreaDataT *)w->data;
    -    AppContextT   *ctx     = wgtGetContext(w);
         int32_t        visRows = 1;
     
    -    if (ctx) {
    +    if (wgtGetContext(w)) {
             TextAreaGeomT geom;
     
    -        textAreaComputeGeom(w, &ctx->font, &geom);
    +        textAreaComputeGeom(w, wgtFont(w), &geom);
             visRows = DVX_MAX(geom.visRows, 1);
         }
     
    @@ -1443,7 +1435,7 @@ int32_t wgtTextAreaReplaceAll(WidgetT *w, const char *needle, const char *replac
         int32_t count     = textEditReplaceAll(&ta->te.lines, ta->te.buf, ta->te.bufSize, &ta->te.len, needle, replacement, caseSensitive, &ta->te.cursorRow, &ta->te.cursorCol, &ta->te.desiredCol, &ta->te.selAnchor, &ta->te.selCursor, ta->te.undoBuf, &ta->te.undoLen, &ta->te.undoCursor);
     
         if (count > 0) {
    -        ta->cachedGutterW = 0;
    +        ta->cachedGutterCols = 0;
             wgtInvalidatePaint(w);
             widgetFireChange(w);
         }
    @@ -1465,7 +1457,7 @@ bool wgtTextAreaReplaceSelection(WidgetT *w, const char *needle, const char *rep
         bool replaced     = textEditReplaceSelectionMulti(&ta->te.lines, ta->te.buf, ta->te.bufSize, &ta->te.len, needle, replacement, caseSensitive, forward, &ta->te.cursorRow, &ta->te.cursorCol, &ta->te.desiredCol, &ta->te.selAnchor, &ta->te.selCursor, ta->te.undoBuf, &ta->te.undoLen, &ta->te.undoCursor);
     
         if (replaced) {
    -        ta->cachedGutterW = 0;
    +        ta->cachedGutterCols = 0;
             wgtInvalidatePaint(w);
     
             if (!widgetFireChange(w)) {
    @@ -2010,8 +2002,7 @@ void widgetTextAreaDestroy(WidgetT *w) {
     static void widgetTextAreaDragSelect(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
         (void)root;
         TextAreaDataT     *ta   = (TextAreaDataT *)w->data;
    -    AppContextT       *ctx  = wgtGetContext(w);
    -    const BitmapFontT *font = &ctx->font;
    +    const BitmapFontT *font = wgtFont(w);
         TextAreaGeomT      geom;
     
         textAreaComputeGeom(w, font, &geom);
    @@ -2051,10 +2042,9 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
     
         clearOtherSelections(w);
     
    -    AppContextT  *ctx = wgtGetContext(w);
         TextAreaGeomT geom;
     
    -    textAreaComputeGeom(w, &ctx->font, &geom);
    +    textAreaComputeGeom(w, wgtFont(w), &geom);
     
         TextEditMultiOptionsT opts = {
             .autoIndent  = ta->autoIndent,
    @@ -2074,7 +2064,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
             return;
         }
     
    -    ta->cachedGutterW = 0;
    +    ta->cachedGutterCols = 0;
     }
     
     
    @@ -2087,12 +2077,12 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
     // is cleared on double/triple click since the selection is already
     // complete.
     void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
    +    (void)root;
         TextAreaDataT *ta = (TextAreaDataT *)w->data;
         sFocusedWidget = w;
         clearOtherSelections(w);
     
    -    AppContextT *ctx = (AppContextT *)root->userData;
    -    const BitmapFontT *font = &ctx->font;
    +    const BitmapFontT *font = wgtFont(w);
     
         TextAreaGeomT geom;
         textAreaComputeGeom(w, font, &geom);
    @@ -2282,11 +2272,10 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
     
     
     static void widgetTextAreaScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) {
    -    TextAreaDataT *ta  = (TextAreaDataT *)w->data;
    -    AppContextT   *ctx = wgtGetContext(w);
    +    TextAreaDataT *ta = (TextAreaDataT *)w->data;
         TextAreaGeomT  geom;
     
    -    textAreaComputeGeom(w, &ctx->font, &geom);
    +    textAreaComputeGeom(w, wgtFont(w), &geom);
     
         if (orient == TEXTAREA_SB_VERT) {
             int32_t sbY = w->y + TEXTAREA_BORDER;
    @@ -2353,10 +2342,10 @@ const char *widgetTextInputGetText(const WidgetT *w) {
     static void widgetTextInputOnDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
         (void)root;
         (void)vy;
    -    TextInputDataT *ti  = (TextInputDataT *)w->data;
    -    AppContextT    *ctx = wgtGetContext(w);
    +    TextInputDataT    *ti   = (TextInputDataT *)w->data;
    +    const BitmapFontT *font = wgtFont(w);
     
    -    widgetTextEditDragUpdateLine(&ti->te, vx, w->x + TEXT_INPUT_PAD, textInputVisibleChars(w), &ctx->font);
    +    widgetTextEditDragUpdateLine(&ti->te, vx, w->x + TEXT_INPUT_PAD, textEditVisibleChars(w->w, font), font);
     }
     
     
    @@ -2395,12 +2384,11 @@ void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
     // drag-select by setting both selStart and selEnd to the click
     // position and registering sDragWidget.
     void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
    +    (void)root;
         TextInputDataT *ti = (TextInputDataT *)w->data;
         sFocusedWidget = w;
         clearOtherSelections(w);
    -
    -    AppContextT *ctx = (AppContextT *)root->userData;
    -    widgetTextEditMouseClick(w, &ti->te, vx, vy, w->x + TEXT_INPUT_PAD, &ctx->font, true, true);
    +    widgetTextEditMouseClick(w, &ti->te, vx, vy, w->x + TEXT_INPUT_PAD, wgtFont(w), true, true);
     }
     
     
    @@ -2427,7 +2415,7 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi
         int32_t textX   = w->x + TEXT_INPUT_PAD;
         int32_t textY   = w->y + (w->h - font->charHeight) / 2;
         int32_t off     = ti->te.scrollOff;
    -    int32_t dispLen = DVX_MAX(DVX_MIN(DVX_MIN(ti->te.len - off, textInputVisibleChars(w)), (int32_t)sizeof(dispBuf)), 0);
    +    int32_t dispLen = DVX_MAX(DVX_MIN(DVX_MIN(ti->te.len - off, textEditVisibleChars(w->w, font)), (int32_t)sizeof(dispBuf)), 0);
     
         if (ti->inputMode == InputPasswordE) {
             memset(dispBuf, ti->maskChar, dispLen);
    @@ -2460,7 +2448,7 @@ void widgetTextInputSetText(WidgetT *w, const char *text) {
     
     static const WidgetClassT sClassTextInput = {
         .version = WGT_CLASS_VERSION,
    -    .flags   = WCLASS_FOCUSABLE,
    +    .flags   = WCLASS_FOCUSABLE | WCLASS_FIXED_FONT,
         .handlers = {
             [WGT_METHOD_PAINT]           = (void *)widgetTextInputPaint,
             [WGT_METHOD_CALC_MIN_SIZE]   = (void *)widgetTextInputCalcMinSize,
    @@ -2476,7 +2464,7 @@ static const WidgetClassT sClassTextInput = {
     
     static const WidgetClassT sClassTextArea = {
         .version = WGT_CLASS_VERSION,
    -    .flags   = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE,
    +    .flags   = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE | WCLASS_FIXED_FONT,
         .handlers = {
             [WGT_METHOD_PAINT]           = (void *)widgetTextAreaPaint,
             [WGT_METHOD_CALC_MIN_SIZE]   = (void *)widgetTextAreaCalcMinSize,
    diff --git a/src/widgets/kpunch/treeView/widgetTreeView.c b/src/widgets/kpunch/treeView/widgetTreeView.c
    index e925b5fb..cf65b651 100644
    --- a/src/widgets/kpunch/treeView/widgetTreeView.c
    +++ b/src/widgets/kpunch/treeView/widgetTreeView.c
    @@ -66,9 +66,10 @@
     //
     // Performance note: calcTreeItemsHeight and calcTreeItemsMaxWidth
     // walk the full visible tree, but results are cached in the TreeView's
    -// cachedTotalHeight/cachedMaxWidth fields. The cache is invalidated
    -// (dimsValid = false) whenever the tree structure changes: item add/
    -// remove, expand/collapse, text change, or drag-reorder.
    +// cachedTotalHeight/cachedMaxWidth fields, measured in dimsFont. The
    +// cache is invalidated (dimsFont zeroed) whenever the tree structure
    +// changes: item add/remove, expand/collapse, text change, or
    +// drag-reorder; a font other than dimsFont measures it again.
     
     #define TREEVIEW_IMPL
     
    @@ -96,25 +97,25 @@ static int32_t sTreeItemTypeId = -1;
     
     typedef struct {
         // Fields are ordered by alignment to minimize struct padding.
    -    int32_t   scrollPos;
    -    int32_t   scrollPosH;
    -    WidgetT  *selectedItem;
    -    WidgetT  *anchorItem;
    -    WidgetT  *dragItem;
    -    WidgetT  *dropTarget;
    -    WidgetT **indexItems;       // depth-first item list for the BASIC index API
    -    int32_t   indexCount;
    -    int32_t   indexCap;
    -    int32_t   cachedTotalHeight;
    -    int32_t   cachedMaxWidth;
    -    int32_t   sbDragOrient;
    -    int32_t   sbDragOff;
    -    bool      multiSelect;
    -    bool      reorderable;
    -    bool      dropAfter;
    -    bool      dropInto;
    -    bool      dimsValid;
    -    bool      indexValid;       // false after any structural change
    +    int32_t            scrollPos;
    +    int32_t            scrollPosH;
    +    WidgetT           *selectedItem;
    +    WidgetT           *anchorItem;
    +    WidgetT           *dragItem;
    +    WidgetT           *dropTarget;
    +    WidgetT          **indexItems;  // depth-first item list for the BASIC index API
    +    FontStampT         dimsFont;    // font the cached dims were measured in (zeroed: stale)
    +    int32_t            indexCount;
    +    int32_t            indexCap;
    +    int32_t            cachedTotalHeight;
    +    int32_t            cachedMaxWidth;
    +    int32_t            sbDragOrient;
    +    int32_t            sbDragOff;
    +    bool               multiSelect;
    +    bool               reorderable;
    +    bool               dropAfter;
    +    bool               dropInto;
    +    bool               indexValid;  // false after any structural change
     } TreeViewDataT;
     
     typedef struct {
    @@ -444,9 +445,7 @@ static int32_t calcTreeItemsHeight(WidgetT *parent, const BitmapFontT *font) {
     
     
     // Finds the widest visible item accounting for depth-based indent.
    -// Used to determine if a horizontal scrollbar is needed. The text
    -// width uses strlen * charWidth (monospace font assumption) rather
    -// than a proportional measurement function.
    +// Used to determine if a horizontal scrollbar is needed.
     static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, int32_t depth) {
         int32_t maxW = 0;
     
    @@ -457,7 +456,7 @@ static int32_t calcTreeItemsMaxWidth(WidgetT *parent, const BitmapFontT *font, i
     
             TreeItemDataT *ti     = (TreeItemDataT *)c->data;
             int32_t        indent = depth * TREE_INDENT + TREE_EXPAND_SIZE + TREE_ICON_GAP;
    -        int32_t        textW  = (int32_t)(ti->text ? strlen(ti->text) : 0) * font->charWidth;
    +        int32_t        textW  = ti->text ? textWidth(font, ti->text) : 0;
             int32_t        itemW  = indent + textW;
     
             if (itemW > maxW) {
    @@ -514,8 +513,8 @@ static void invalidateTreeDims(WidgetT *w) {
     
         if (tree) {
             TreeViewDataT *tv = (TreeViewDataT *)tree->data;
    -        tv->dimsValid  = false;
    -        tv->indexValid = false;
    +        tv->dimsFont.font = NULL;
    +        tv->indexValid    = false;
         }
     }
     
    @@ -845,13 +844,14 @@ static void treeApplyScrollHit(WidgetT *w, int32_t orient, int32_t sbLen, int32_
     
     // Compute content dimensions and determine which scrollbars are
     // needed, accounting for the mutual space dependency between them.
    +// The cached dimensions are measured again when stale or in another font.
     static void treeCalcScrollbarNeeds(WidgetT *w, const BitmapFontT *font, int32_t *outTotalH, int32_t *outTotalW, int32_t *outInnerH, int32_t *outInnerW, bool *outNeedVSb, bool *outNeedHSb) {
         TreeViewDataT *tv = (TreeViewDataT *)w->data;
     
    -    if (!tv->dimsValid) {
    +    if (!fontStampIs(&tv->dimsFont, font)) {
             tv->cachedTotalHeight = calcTreeItemsHeight(w, font);
             tv->cachedMaxWidth    = calcTreeItemsMaxWidth(w, font, 0);
    -        tv->dimsValid         = true;
    +        fontStampSet(&tv->dimsFont, font);
         }
     
         *outTotalH = tv->cachedTotalHeight;
    @@ -1020,7 +1020,7 @@ static bool treeSetExpanded(WidgetT *item, bool expanded) {
         }
     
         TreeViewDataT *tv = (TreeViewDataT *)tree->data;
    -    tv->dimsValid = false;
    +    tv->dimsFont.font = NULL;
     
         if (expanded) {
             return true;
    @@ -1210,7 +1210,7 @@ void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item) {
         }
     
         if (expandedAny) {
    -        tv->dimsValid = false;
    +        tv->dimsFont.font = NULL;
         }
     
         if (!setSelectedItem(w, item)) {
    @@ -1230,7 +1230,7 @@ void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item) {
         AppContextT *ctx = wgtGetContext(w);
     
         if (item && ctx) {
    -        treeScrollItemIntoView(w, &ctx->font, item);
    +        treeScrollItemIntoView(w, wgtFontFor(w, &ctx->font), item);
         }
     
         // Only an ancestor expansion changes the layout; a plain selection
    @@ -1361,8 +1361,8 @@ void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) {
     void widgetTreeViewOnChildChanged(WidgetT *parent, WidgetT *child) {
         (void)child;
         TreeViewDataT *tv = (TreeViewDataT *)parent->data;
    -    tv->dimsValid  = false;
    -    tv->indexValid = false;
    +    tv->dimsFont.font = NULL;
    +    tv->indexValid    = false;
     
         // This fires for BOTH wgtDestroy and wgtSetVisible, so it must not
         // clear the selection/drag pointers here: a mere visibility toggle
    @@ -1530,7 +1530,7 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
     
         // Scroll to keep the cursor item visible
         if (tv->selectedItem) {
    -        treeScrollItemIntoView(w, &wgtGetContext(w)->font, tv->selectedItem);
    +        treeScrollItemIntoView(w, wgtFont(w), tv->selectedItem);
         }
     
         // Item rows only move when a node expands or collapses; cursor and
    @@ -1560,7 +1560,7 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
     void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
         TreeViewDataT     *tv   = (TreeViewDataT *)hit->data;
         AppContextT       *ctx  = (AppContextT *)root->userData;
    -    const BitmapFontT *font = &ctx->font;
    +    const BitmapFontT *font = wgtFontFor(hit, &ctx->font);
     
         sFocusedWidget = hit;
     
    @@ -1793,8 +1793,8 @@ static bool widgetTreeViewReorderDrop(WidgetT *w) {
             }
         }
     
    -    tv->dimsValid  = false;
    -    tv->indexValid = false;
    +    tv->dimsFont.font = NULL;
    +    tv->indexValid    = false;
         return widgetFireChange(w);
     }
     
    @@ -1803,8 +1803,7 @@ static void widgetTreeViewReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, in
         (void)root;
         (void)x;
         TreeViewDataT     *tv     = (TreeViewDataT *)w->data;
    -    AppContextT       *ctx    = wgtGetContext(w);
    -    const BitmapFontT *font   = &ctx->font;
    +    const BitmapFontT *font   = wgtFont(w);
         int32_t            innerY = w->y + TREE_BORDER;
         int32_t            relY   = y - innerY + tv->scrollPos;
     
    @@ -1863,8 +1862,7 @@ static void widgetTreeViewReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, in
     // Handle scrollbar thumb drag for vertical and horizontal scrollbars.
     static void widgetTreeViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) {
         TreeViewDataT     *tv   = (TreeViewDataT *)w->data;
    -    AppContextT       *ctx  = wgtGetContext(w);
    -    const BitmapFontT *font = &ctx->font;
    +    const BitmapFontT *font = wgtFont(w);
     
         int32_t totalH;
         int32_t totalW;
    diff --git a/src/widgets/kpunch/wgtsys.dhs b/src/widgets/kpunch/wgtsys.dhs
    index 4c3408e6..c4f43816 100644
    --- a/src/widgets/kpunch/wgtsys.dhs
    +++ b/src/widgets/kpunch/wgtsys.dhs
    @@ -118,7 +118,7 @@ wgtFireEvent(w, name, args, argc) fires an event by name: the names above (Click
     .index wgtSetProp
     .index wgtCallMethod
     
    -Every widget type can be created, read, written and called by name through its interface descriptor, with values as WgtValueT -- the way the BASIC form runtime and the designer drive widgets, and the way a C program uses a widget it has no header for (a BASIC control, for one):
    +Every widget type can be created, read, written, and called by name through its interface descriptor, with values as WgtValueT -- the way the BASIC form runtime and the designer drive widgets, and the way a C program uses a widget it has no header for (a BASIC control, for one):
     
     .code
     WidgetT  *list = wgtCreateByName("ListBox", parent);