Skip to content
Jan Boon edited this page Jul 24, 2026 · 3 revisions

title: Lua Scripting description: The interface Lua API, widget reflection, and building whole applications in interface data published: true editor: markdown

Lua is the scripting layer of the interface: scripts see every widget as a live object, can rewire anything at runtime, and can carry entire applications. The showcase's Minesweeper, drag-and-drop inventory and the console below are pure XML+Lua — no game code in the binary.

The Lua console demo: evaluate an expression, inspect a group with ls(), read a live widget property

Loading scripts

<lua file="minesweeper.lua" />

<lua> elements execute at parse time, in order, in one shared Lua state. A script error is fatal to interface loading (everything else in the parser merely warns — scripts are assumed load-bearing). Functions defined in one file are visible to all later files and to every handler.

Scripts hook into the UI through the lua action handler: onclick_l="lua" params_l="game.onClick(1)", onenter="lua" params="console.enter()", and the engine itself runs setOnDraw scripts and lua_class associations through the same handler.

The API

What the library registers (available in any embedding that calls initLUA):

UI manipulationsetOnDraw(group, "script") (per-frame tick; the Minesweeper timer and the inventory gesture loop live here), getOnDraw, addOnDbChange(group, "@UI:A,@UI:B", "script") / removeOnDbChange (react to database changes), runAH(ctrlOrNil, "ah", "params"), runExpr("expr") / runFct("fct", ...) (interface expressions), getUIId(elt), deleteUI(elt), setCaptureKeyboard(ctrl) / resetCaptureKeyboard, setTopWindow(win), disableModalWindow, getCurrentWindowUnder(), getWindowSize(), getTextureSize("name").

EnvironmentrunCommand (NeL console commands), fileExists, fileLookup, getPathContent (a filesystem listing — nothing to do with UI children), nltime.getPreciseLocalTime() (seconds, high resolution), nltime.getLocalTime() (ms), nltime.getSecondsSince1970(), nlfile.*, i18n.get("uiKey"), concatString, findReplaceAll, and the CRGBA/CVector2f classes.

From the embeddergetUI("ui:path") is not part of the library: every application supplies its own widget lookup (the client's does extra work; the sample registers a minimal one). The client additionally provides getUICaller(), getDbProp/setDbProp, formatUI, dumpUI, messageBox, createGroupInstance and a large game-side surface — check what your host registers. The sample adds getMousePos()/getMouseDown() for gesture work (below).

Lua 5.1 semantics apply: math.fmod not % on floats in old idioms, #t for length, loadstring for eval.

Widgets as objects: reflection

getUI returns a userdata wrapping the element. Three things work on it:

Properties — every reflected property reads and writes as a field: w.active = false, txt.hardtext = 'Hello', bmp.color = '150 255 150 255', eb.input_string, plus the read-only computed geometry x_real/y_real/w_real/h_real (see Layout). Wrong property names read as nil (safe to probe) but assigning an unknown property raises an error.

Child navigation — child elements are fields: win.content.buttons.ok, cell.cover.active = false. elt.parent walks up. elt.isNil is true if the underlying C++ object has been destroyed (the wrapper is a safe reference, not ownership).

Enumeration — widget userdata implement the __next metamethod: redefine next/pairs to honor it (the Ring scripts do exactly this) and pairs(widget) iterates children first (groups, then views, then ctrls — keys stringify like "_Group 3", values are the child widgets), then every reflected property (keys stringify to the property names):

local oldNext = next
function next(t, k)
	local mt = getmetatable(t)
	if mt and mt.__next then return mt.__next(t, k) end
	return oldNext(t, k)
end
function pairs(t) return next, t, nil end

for k, v in pairs(getUI('ui:sample:views')) do
	print(tostring(k), tostring(v))
end

This is the foundation of the console's ls() (children by kind) and props() (property dump) helpers — a complete UI inspector in ~40 lines of Lua.

Patterns from the demo apps

Reflection-driven game state: Minesweeper

Minesweeper mid-game: 81 template-stamped cells driven entirely from Lua

The board is a <vector> of 81 cell templates; all rules live in Lua. A cell reveal is just cell.cover.active = false; flags toggle a marker bitmap; the timer is a setOnDraw script comparing nltime.getPreciseLocalTime(). Buttons pass their index through their params: onclick_l="lua" params_l="mines.reveal($i)" stamped by the vector.

A REPL over the live tree: the console

The console log is a vector of text views repainted from a history table; the input is a stock edit box with onenter="lua" params="console.enter()" and enter_loose_focus="false" (the trap). Evaluation tries loadstring('return '..line) first so expressions print their value, then falls back to statement form; print is redirected into the log. Because it runs in the same state as every other script, it can poke any running demo live — indispensable for debugging wiring.

Drag and drop: the inventory pattern

Mid-drag: the ghost icon follows the cursor and the valid target slot glows After the drop: the helmet sits in its typed slot

The library has no drag widget; the inventory does gestures in data using two embedder hooks and a per-frame tick:

-- inside setOnDraw of the inventory window
local mx, my = getMousePos()
local down = getMouseDown()
-- press edge: hit-test slots against x_real/y_real/w_real/h_real
-- while dragging: ghost.x = mx - content.x_real - 20 (BL-anchored ghost bitmap)
-- release edge: accept/swap/reject by comparing slot kind tags in Lua

Slots are plain groups; the dragged item is a render_layer-raised ghost bitmap; the glow is an active toggle on a highlight bitmap; acceptance rules are table lookups. Coordinates from the mouse hooks are interface pixels — the same space as the reflected *_real geometry, so hit-testing is a four-comparison function. The hooks themselves (getMousePos, getMouseDown, and the pointer button mirroring they depend on) are part of the embedding contract.

Debugging tips

  • Ship a console (the sample's is ~150 lines of XML+Lua, reusable) — live inspection beats log archaeology.
  • runAH from the console exercises any wiring exactly as a click would.
  • Parse-time scripts can call getUI on elements declared earlier in the parse; defer anything touching later elements to a first setOnDraw tick or an on_active handler.
  • Errors inside AH-invoked scripts land in the application log, not on screen — keep it visible while iterating.

Clone this wiki locally