OPX//77
opx_infinitymenu

Client

Last updated on

The menu contract on the client — open, update and close a list menu, and the spec it accepts.

local menu = OPX.Api.Get('menu')

From client code inside opx_infinity. Every function answers a Result ({ ok = true, value } or { ok = false, error }). None yields, unless the spec opts in with yield = true. Codes: Refusal codes.

Open

Opens a menu from a spec. The spec is refused whole on the first bad field.

menu.Open(spec)

Parameters

Returns

  • Result — value { handle, id, nodes }

  • Another owner's menu open: menu_busy, unless spec.steal = true.

  • The same owner opening again replaces its menu (close reason reopened).

Example
local menu = OPX.Api.Get('menu')
local opened = menu.Open({
  owner = 'mymodule',
  title = 'GARAGE',
  items = {
    { id = 'out', label = 'Take out', icon = 'vehicle' },
    { id = 'lights', label = 'Lights', toggle = false },
    { label = 'Close', close = true },
  },
  on = function(p)
    if p.action == 'select' and p.itemId == 'out' then --[[ ... ]] end
  end,
})
if not opened.ok then print(opened.error) end

Close

Closes the menu.

menu.Close(handle, reason)

Parameters

  • handle: integer — required (handle_required)
  • reason?: string
    • Max 32 characters, passed to the close callback. Default caller.

Returns

  • Result — value true

Update

Rebuilds the open menu and keeps the player's position (submenus stay open while a row with the same id still exists).

menu.Update(handle, patch)
menu.Update(patch)

Parameters

  • handle?: integer
  • patch: table
    • Patchable: items, title, on, data, closeOnSelect, reportFocus, status, statusBad, cursor.
    • items is all or nothing. Geometry and focus are fixed at open.

Returns

  • Result — value true
Example
menu.Update(opened.value.handle, { title = 'GARAGE (2)' })

SetStatus

Shows text as a toast. There is no status line under the list any more.

menu.SetStatus(handle, text, bad)

Parameters

  • handle?: integer
  • text: string — max 120 characters
  • bad?: boolean — red when true

Returns

  • Result — value true

State

What is on screen now.

menu.State()

Returns

  • Result — value { open = false } or { open, handle, owner, menu, title, depth, index, total, itemId, label, value }

Keys

For a caller printing its own key hint.

menu.Keys()

Returns

  • Result — value { backend, keys, labels }
    • backend: page, or none when no surface exists
    • keys: maps UP DOWN LEFT RIGHT SELECT BACK to ARROW UP … ENTER, BACKSPACE
    • labels: the locale words

The menu spec

FieldTypeDefaultMeaning
ownerstringrequiredYour module id. Letters, digits, _ : - ., max 64. If it names a module that stops, the menu closes (owner_stopped).
onfunctionrequiredon(payload) for every action. See The action payload.
itemslistrequiredThe rows of the root level. See Rows.
idstringownerMenu id, echoed as payload.menu.
titlestringowner upper-casedMax 96 characters.
datatable—Echoed as payload.menuData. Max 64 value nodes, 4 levels deep.
focusstringnonenone: no input focus, the player keeps walking; Lua reads the six keys itself. cursor: the page gets the mouse. full: the page gets keyboard and mouse. true/false read as none.
closablebooleantruefalse: Escape does not close it; the owner must call Close.
closeOnSelectbooleanfalseClose after any select.
reportFocusbooleanfalseRaise a focus action each time the cursor lands on a row.
cursorstring or integerfirst selectable rowRow id or index to start on.
stealbooleanfalseReplace another owner's open menu.
status, statusBadstring, boolean—Shown as a toast at open.
anchorstringANCHORtop-left, top-right, left, right or center.
widthintegerWIDTHPixels, clamped 240..1200.
maxHeightintegerMAX_HEIGHT_VHViewport height %, clamped 20..100.
heightintegernoneFixed pixel height, clamped 120..1200. Absent: the rows decide.
rowsintegerVISIBLE_ROWSRows drawn at once, clamped 3..24.
yieldbooleanfalseOpen/Update only. When the caller runs on its own thread, rows are checked six at a time with a frame between, so a big menu stays inside one resume's instruction budget. The surface, the down state, menu_busy (or for Update, no_menu_open/stale_handle) are re-checked before the menu is installed. Ignored where the caller cannot yield.

Limits: 200 rows per level, 400 rows across the whole tree, 8 levels deep. Only the visible window of rows is sent to the page. Checking a spec costs roughly 240 instructions per row: for more than a few dozen rows, page them or pass yield = true from a thread.

Example — a long list from a thread
CreateThread(function()
  local opened = menu.Open({ owner = 'mymodule', items = rows, on = onAction, yield = true })
  if not opened.ok then print(opened.error) end
end)

Rows

A row's kind comes from its shape. Tested in this order; the first match wins.

KindShapeBehaviour
separatorseparator = true, optional labelA divider. Cannot be selected. Other fields are ignored.
submenuitems = { … }, optional title, value, cursorEnter or Right opens the level (open action).
toggletoggle = true/false, optional onLabel, offLabelEnter, Left or Right flips it (change). value is the boolean.
choiceschoices = { 'a', 'b' }, optional selected (index)Left/Right cycles (change). value is the chosen string.
sliderslider = { min = 0, max = 100, step = 1, value, suffix }Left/Right steps and clamps (change). value is the number. max must be above min.
backback = truePops one level, or closes at the root. Escape does the same: it goes back one level and closes only at the top.
closeclose = true and no actCloses the menu (reason item).
actionanything elseEnter raises select. value (max 48) is the right-hand text.

Fields every row may carry:

FieldMeaning
idRow id, echoed as itemId. Default item_<index>.
label (or text)Required, max 96 characters.
iconA name from OPX.Glyphs; an unknown name refuses the spec (invalid_item_icon).
descriptionHint shown for the focused row, max 160.
dataEchoed as payload.data. Max 64 nodes, 4 deep.
disabledDrawn greyed; the cursor skips it.
actact(payload), called before on for this row only.
closeOn an action row with act: close after it fires.
submenutrue draws the arrow on a row with no items of its own (for owners that open one flat screen at a time). It still raises select.
confirmOn a row with no items: true or { title?, yes?, no? }. See Confirm rows.

Confirm rows

A row with confirm opens a two-row level instead of acting. The first row keeps things as they are (a back row, so Enter pressed without looking does nothing); the second does what the row said, under the same id and data, so on receives the select it always did.

FieldDefault (en / fr)
title{label}? / {label} ? (menu.confirm.title)
noNo, keep it / Non, garder (menu.confirm.no)
yesYes, {label} / Oui : {label} (menu.confirm.yes)
Example
items = {
  { id = 'delete', label = 'Delete outfit', icon = 'trash', confirm = { yes = 'Delete outfit', no = 'Keep it' } },
  { id = 'sell', label = 'Sell', confirm = true },
},
on = function(p)
  if p.action == 'select' and p.itemId == 'delete' then --[[ only after the second row ]] end
end,

The action payload

on(payload) and the opx:on:menu:action event receive the same table.

FieldMeaning
menu, handle, ownerWhich menu.
actionselect, change, open, back, focus or close.
itemId, label, value, dataThe row involved. Absent on close and on a back from Left/Backspace.
index, depthCursor position on the current screen (1-based) and stack depth (1 at the root).
menuDataThe spec's data.
reasonOn close: caller (or your own reason), select, item, back, dismissed, pause, reopened, superseded, owner_stopped, player_down, stopped.
repeatedOn focus: true when the key is held down.

On this page