widgettype = "label"
Static text. One line at a point by default; give it w and h and it wraps at word boundaries inside that box instead, clipping when it runs out of height. Size h for the longest text the label can be given — at font f each line costs f + 3, and the last one fits while it starts at or before h - f. Undersize it and the tail vanishes with nothing to show for it; the engine logs label doesn't fit its w/h to the developer console when that happens.
| Field | Type | Notes |
| x, y | number | Position within the panel. |
| w, h | number? | Optional. Both set = wrap inside this box. Omit for a single unwrapped line. |
| text | string | The text to show. \n starts a new line when wrapping. |
| hidden | boolean? | Skip drawing entirely. |
| style | table? | { fg = {…}, font = number } |
widgettype = "row"
A layout container: give it a height and a list of children, and the engine places them. Each child keeps its own x and w, and the row assigns its y — vertically centred, with a label measured at the width it will actually wrap to rather than assumed to be one line. A child is never placed above the row's top: one taller than its row starts at the top and clips there instead of bleeding into the row above.
The row draws its own background, which is what makes list striping a property of the row rather than a rectangle somebody has to keep aligned with it. Rows are layout, not widgets: they take no index of their own, so a child's position in children — for overlay.focus, for tab order — is the same number whether or not you later wrap it in one.
Use a row for anything with a left label and controls on the right. The alternative is computing each child's y by hand, and a hand-computed offset is a guess that silently goes wrong the first time a title wraps to a second line.
| Field | Type | Notes |
| x, y, w, h | number | Where the row sits in the panel, and how tall it is. Children are positioned inside it. |
| children | table | Widgets. Their y is the engine's — anything you set is overwritten. |
| hidden | boolean? | Hides the row and everything in it. |
| style | table? | { bg, border, bgImage }. Omit bg for no background. Theme section: row. |
-- a striped list: only the band differs between rows
for i = 1, ROWS do
kids[#kids+1] = {
type = "row", x = 12, w = 400, h = 34,
style = { bg = (i % 2 == 0) and {255,255,255,12} or nil },
children = {
{ type = "label", x = 8, w = 260, h = 34, text = "" },
{ type = "button", x = 290, w = 100, h = 26, text = "Go" },
},
}
end
-- ...and at layout time the panel sets ONE number per row:
row.y = y ; y = y + 34
widgettype = "rect"
A filled rectangle. No text, no interaction, and no click area — a press goes straight through to whatever is under it, so it can sit behind other widgets without stealing their clicks. It exists for lists: children draw in the order they are added, so a rect added just before a row's own widgets becomes that row's background band. Striping every other row is what lets the eye carry a long title across to the buttons that belong to it — the engine's own My mods and Download mods lists use it exactly that way.
| Field | Type | Notes |
| x, y, w, h | number | Position & size within the panel. |
| hidden | boolean? | Skip drawing entirely. |
| style | table? | { bg = {…}, border = {…}, bgImage = "path" }. The border is only drawn when you give it one; the fill defaults to a faint white wash that reads on any panel colour. Theme section: rect. |
-- every other row gets a band, added BEFORE that row's widgets
for i = 1, ROWS do
if i % 2 == 0 then
kids[#kids+1] = { type = "rect", x = 12, w = 400, h = 30,
style = { bg = {255,255,255,12} } }
end
kids[#kids+1] = { type = "label", x = 16, w = 260, h = 30, text = "" }
kids[#kids+1] = { type = "button", x = 290, w = 100, h = 26, text = "Go" }
end
widgettype = "textbox"
An editable text field. Click it (or call overlay.focus) to focus; the engine handles typing, the blinking caret, and editing keys natively. Read/write its current contents via the text field.
| Field | Type | Notes |
| x, y, w, h | number | Position & size within the panel. |
| text | string | Current contents (updated live as the user types). |
| caret | number? | Caret position as a byte offset into text (0 = before the first character). Maintained by the engine; if you replace text yourself, set it too (or leave it — it is clamped and snapped to a character boundary). Defaults to the end. |
| sel | number? | Selection anchor, a byte offset like caret; the selected span is everything between the two. nil (or equal to caret) means nothing is selected. Maintained by the engine — set it yourself only to pre-select something. |
| placeholder | string? | Shown dimmed when empty and unfocused. |
| disabled | boolean? | Draw the box dimmed and refuse focus: it is shown, not offered. Clicking it drops focus like clicking the panel background, and Tab skips it. Use this rather than hidden when the field's label still explains what would go in it. |
| multiline | boolean? | If true, Enter inserts a newline; otherwise Enter defocuses. |
| password | boolean? | Draw one * per character instead of the text. text still holds the real value. |
| onChange | function? | Called with the new text whenever it changes. |
| hidden | boolean? | Skip drawing and hit-testing entirely. |
| style | table? | { bg, fg, border, font } |
Editing keys: typing inserts at the caret; a click puts the caret where you clicked. Backspace/Delete remove around the caret, ←/→ move it, Home/End jump within the current line, ↑/↓ change line in a multiline box (keeping the column), Ctrl+V pastes at the caret (capped at 4096 characters, replacing the selection if there is one), Tab jumps to the next textbox in the same panel (wrapping round, skipping hidden ones), Enter = newline (multiline) or defocus, Esc = defocus. Tab order is the order the widgets appear in children. Only textboxes take focus — buttons can't be pressed from the keyboard, so tabbing onto one would be a dead end. Text input requires the app window to have OS keyboard focus.
Focus decides, not the overlay. A textbox in a pinned or hud panel can be clicked into while the overlay is closed, and it keeps receiving keys there: the panel is on screen during gameplay, so it is typeable during gameplay. While a field holds focus the keystrokes are text and nothing else — "KeyPress" keybinds do not fire from the letters being typed, and Esc lets go of the field instead of closing the program. Click elsewhere or press Esc to give it back.
Overflow scrolls instead of clipping: a single-line box slides horizontally to keep the caret in view, a multiline box scrolls by lines — the caret pulls its line on screen when it moves, the mouse wheel scrolls the box under the pointer, and small corner ticks mark more text above or below. No field for any of this: the engine owns it.
Selecting: drag through the text, or hold Shift while pressing an arrow / Home / End, or Ctrl+A for all of it. The selection is drawn as a highlight behind the glyphs and follows the pointer even if it leaves the field. Typing or pasting replaces it, Backspace/Delete remove it, an unshifted move collapses it. Ctrl+C copies and Ctrl+X cuts.
Copy and cut are refused on a password field, deliberately: the field most in need of pasting is a password one, and putting a password back onto the clipboard is a step in the wrong direction. Pasted control characters are dropped, and newlines survive only in a multiline box.
Use password = true for any secret. This app exists to put a screen in front of other people, and often a recording of it — a visible password field is on camera by definition. Masking is the engine's job, so don't substitute your own characters into text: you'd break what the user is actually typing.
widgettype = "combo"
A dropdown. Shows the selected option; clicking opens a list of options (drawn on top of everything), and picking one collapses it.
| Field | Type | Notes |
| x, y, w, h | number | Position & size of the collapsed box within the panel. |
| options | string[] | The list of choices (a Lua array of strings). |
| selected | number | 1-based index of the current choice. Updated for you when the user picks. |
| onSelect | function? | Called as onSelect(index, value) when a choice is made. |
| style | table? | { bg, fg, border, font } |
Lists longer than 12 options are paged, with a < 3/17 (196) > row under the list — click either half to turn the page. Opening the dropdown jumps to the page holding the current selection. You don't have to do anything for this; hand it as many options as you like.
widgettype = "multi"
The same dropdown with more than one answer. The collapsed box names what is chosen — as many names as fit, then +2 for the rest, so the first names are still the answer and the number is only what is missing from it. The open list gives every option a tick box, and clicking one toggles it and leaves the list open — picking several things is the reason this exists, and closing after each would make it a slower "combo". Click outside to close. Paged the same way past 12 options.
| Field | Type | Notes |
| x, y, w, h | number | Position & size of the collapsed box within the panel. |
| options | string[] | Everything that can be picked. |
| values | string[] | What is picked now. Rewritten for you on every toggle, always in options order whatever order they were clicked in. |
| onChange | function? | Called as onChange(values) with the new array. |
| placeholder | string? | Shown dimmed when nothing is chosen. Defaults to "None". |
| style | table? | { bg, fg, border, placeholder, font } |
widgettype = "slider"
A draggable value bar. Click or drag the track to set the value.
| Field | Type | Notes |
| x, y, w, h | number | Position & size within the panel. |
| min, max | number | Value range. Default 0 and 1. |
| value | number | Current value. Updated for you as the user drags. |
| onChange | function? | Called as onChange(value) while dragging. |
| style | table? | { bg, fill, border } |
A panel with a label, a textbox, and a button:
overlay.add("mymod.form", {
mode = "menu", x = 40, y = 60, w = 260, h = 120,
style = { bg = {30,32,42,235}, border = {120,130,160,255} },
children = {
{ type = "label", x = 12, y = 10, text = "Name:",
style = { fg = {230,230,240}, font = 16 } },
{ type = "textbox", x = 12, y = 34, w = 236, h = 30,
placeholder = "type here",
onChange = function(t) log("name = " .. t) end },
{ type = "button", x = 12, y = 74, w = 100, h = 32, text = "OK",
onClick = function() overlay.close() end,
style = { bg = {56,60,78}, bgHover = {74,80,104} } },
},
})