OPX//77
opx_infinityConcepts

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

LayerWhat goes thereFocus
overlayHUD, toasts, prompts, name tagsnever takes the keyboard or the pointer
modalanything the player drives: menu, form, panel, inventorytakes focus while open
coverthe loading cover, on top of everythingnever 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.

CallDirectionUse
OPX.UI.Send(target, channel, payload)Lua → pagepush state or open a view
OPX.UI.On(target, channel, handler)page → Luareceive an action; the handler only gets table payloads
  • Each view emits <module>:ready when 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 diag channel; 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.

HelperAnswers
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
A view's apply
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:

CallEffect
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 VIEW event (on the opx: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.

On this page