The WebUI page
Last updated on
Every screen in opx_infinity is drawn by one CEF page built from Vue sources; Lua talks to it on named channels through OPX.UI, and modules keep their rules apart from the view.
Everything a player sees from opx_infinity — HUD, menus, inventory, toasts, name tags — is drawn by one CEF page, web/index.html, built from the Vue sources in ui/src/ with npm run build (only web/ ships). Lua talks to it through OPX.UI.
Layers in one page
| Layer | What goes there | Focus |
|---|---|---|
overlay | HUD, toasts, prompts, name tags | never takes the keyboard or the pointer |
modal | anything the player drives: menu, form, panel, inventory | takes focus while open |
cover | the loading cover, on top of everything | never takes the pointer; nothing is mounted while it is down |
A view registered with hideWhileLoading: true (HUD, toasts, prompts, progress, slot bar, tags, chat log, call hologram, target eye) is hidden with v-show while the game's loading screen is up, and comes back exactly as its module left it.
The built page is about 542 kB (12 % lighter since the UI audit): unused augmented-ui rules are pruned at build time, unused font weights are not inlined, and the Vue Options API is compiled out. Write components with <script setup>.
The client boot creates the page once (OPX.UI.Surface()), before the modules start, with OPX.Config.CLIENT.SURFACE.
Channels
Lua and the page exchange messages on channels named <module>:<verb> (e.g. menu:open, menu:choose). The surface adds an opx: prefix: the page sees opx:menu:open.
| Call | Direction | Use |
|---|---|---|
OPX.UI.Send(target, channel, payload) | Lua → page | push state or open a view |
OPX.UI.On(target, channel, handler) | page → Lua | receive an action; the handler only gets table payloads |
- Each view emits
<module>:readywhen it mounts. A send made before the page is ready is dropped, not queued. - Each module's events page lists its channels under Page channels; the core's own are in Runtime events.
- The page reports its JavaScript errors on the
diagchannel; the diagnostics module forwards them to the server journal. The CEF console is visible nowhere else.
Reading payloads
A payload comes from Lua and may be malformed: a Lua sequence with a hole arrives as [a, null, b], and a key such as "constructor" reads an inherited function from an object literal. Read every payload through the helpers in ui/src/bridge/types.ts, never directly.
| Helper | Answers |
|---|---|
text(v, fallback), num(v, fallback), bool(v, fallback) | the value if it has the type, else the fallback |
table(v) | a map, {} for anything else |
list(v) | an array, [] for anything else |
records(v) | a list of maps: each element passed through table() in place, so a null hole becomes {} and row indexes are kept |
own(map, key) | map[key] for the map's own keys only, else undefined |
import { records, own, text, type Payload } from '@/bridge/types'
const ICONS: Record<string, string> = { car: 'vehicle', door: 'lock' }
function apply(payload: Payload) {
rows.value = records(payload.rows).map((row) => ({
id: text(row.id), // '' if missing
icon: own(ICONS, text(row.kind)) ?? 'interact', // 'constructor' -> fallback, not a function
}))
}Focus
Several views may want the keyboard at once, so the client keeps a focus stack:
| Call | Effect |
|---|---|
OPX.UI.AcquireFocus(owner, { keyboard, cursor }) | puts owner on top; keyboard defaults to true, cursor to false |
OPX.UI.ReleaseFocus(owner) | removes owner; focus falls to the next one |
OPX.UI.FocusOwner() | who holds it now |
OPX.UI.ReleaseAllFocus() | empties the stack; the first step of the client stop |
The Escape key belongs to the platform's open77_pause; never bind it. It raises open77:pauseKey to every listener, and since PR #95 only the top layer closes: the menu, form, panel, door panel, chat box and call hologram each enter OPX.Spots.Key.Layer, and the one opened last takes the press. A menu over the hologram closes alone; a form over a menu closes and leaves the menu. Inside a submenu, Escape goes back one level. A view of your own should do the same with TopLayer.
Keep rules and drawing apart
- A module owns its state and rules but does not draw. It raises its
VIEWevent (on theopx:on:channel) with what should be shown, and takes the player's actions back through one function,M.FromView(action, payload). - One file, usually
client/view.lua, is the only one that knows the other end is a CEF page. Reference:modules/chat/client/view.lua. - Before writing a new Vue view, check whether menu, form or panel can draw it — they are services any module can use through their contracts.
Colours come from the theme
Never hard-code a colour in a view. The theme module sends the server's palette (config/theme.lua) to the page, which writes it as CSS custom properties on :root. ui/README.md in the framework is the binding style guide for views.
Two runtimes
The Open77 dedicated server and game client run Lua differently — no require on the server, a per-resume instruction budget on the client, no metatables in shared code.
Storage
How opx_infinity reads and writes the database through OPX.Storage, how modules add their tables and indexes with OPX.Schema, and what OPX.Carry keeps across a resource reload.