UI
Add custom widgets to the menu. Every widget call takes (tab, section, name, ...). Create the tab first with ui.new_tab(). A widget call on a missing tab raises "tab does not exist". Sections are created on first use.
Every widget call returns a ref string of the form "tab|section|widget_id". You don't get the value back directly. Read and write through ui.get(ref) and ui.set(ref, value). Values are saved between sessions automatically.
widget_id is the name you passed. If two widgets in one section share a name, the first keeps the bare name and later ones get #2, #3 and so on. ui.get(tab, sec, "Enabled") reads the first one. Reach the others through the returned ref or the suffixed id. Saved config keys use the same id, so adding or removing a duplicate label changes which stored value each widget gets.
Tab and section names must not contain |. A ref is split on its first two | characters, so a | in either name breaks it. A | inside a widget name is fine.
Quick example
ui.new_tab("My ESP", "My ESP")
local on = ui.checkbox("My ESP", "Boxes", "Enabled", true)
local thickness = ui.slider_int("My ESP", "Boxes", "Thickness", 1, 5, 2)
local color = ui.color_picker("My ESP", "Boxes", "Color")
ui.button("My ESP", "Boxes", "Reset", function()
print("reset clicked")
end)
function on_tick()
if not ui.get(on) then return end
for _, p in ipairs(get_players()) do
if p:is_enemy() and p:is_alive() then
local b = p:screen_box()
if b then
draw.rect(b.x, b.y, b.w, b.h,
draw.color(ui.get(color)), ui.get(thickness))
end
end
end
endThe first three arguments ("My ESP", "Boxes", the widget name) determine where the widget appears in the menu. Reuse the same tab+section across calls to group widgets together.
Custom tab labels
ui.new_tab(id, display) takes a group id and a display label. The id is the first argument to every widget call. It groups widgets and stays fixed in code. The display label is what shows in the menu's tab bar, and it can have spaces or special characters. The two can be different:
ui.new_tab("toast_demo", "Toast Demo")
ui.button("toast_demo", "styles", "Default", function()
toast("Hi", 2)
end)| Function | Returns | Description |
|---|---|---|
ui.new_tab(id, label) | - | Registers a tab. Call it before any widget call on this tab, even when id and label are the same, as in ui.new_tab(id, id). An id that another loaded script already registered raises an error |
Windows
| Function | Returns | Description |
|---|---|---|
ui.new_window(id, label, [icon]) | - | Register a floating window instead of a sidebar tab. Widgets attach to it the same way as to a tab, by passing its id as their first argument. icon is a preset glyph name (default "gear") drawn as a navbar button that toggles the window |
A window tab does not appear in the menu sidebar. It starts closed. The user opens it from its navbar button, or closes it with the window's X. Position and size are remembered between launches. Like ui.new_tab, re-registering an id already owned by a different loaded script raises an error.
Preset icon names: gear, wrench, star, crosshair, eye, bolt, code, wand, shield, user, users, map, brush, chart, clock, bell, lock, flask, fire, heart, list, sliders. Any unrecognised name falls back to gear.
ui.new_window("radar_win", "Radar", "map")
local on = ui.checkbox("radar_win", "General", "Enabled", true)Widgets
| Function | Returns | Description |
|---|---|---|
ui.checkbox(tab, section, name, [default], [in_line]) | ref | Toggle. default is a bool. in_line places the widget on the same line as the previous one |
ui.slider_int(tab, section, name, min, max, [default]) | ref | Integer slider. default falls back to min when omitted, not to 0 |
ui.slider_float(tab, section, name, min, max, [default]) | ref | Float slider. default falls back to min when omitted, not to 0 |
ui.dropdown(tab, section, name, options, [default_index]) | ref | Dropdown. options is an array of strings. The selection is returned as a 1-based index |
ui.multiselect(tab, section, name, options) | ref | Multi-select. ui.get returns a boolean array indexed by option position (1-based) |
ui.color_picker(tab, section, name, [default], [in_line]) | ref | Color picker. default is an optional {r,g,b,a} Color table. ui.get returns a Color table {r,g,b,a} (0-255). draw.* needs a packed color, so pass it through draw.color() first |
ui.input_text(tab, section, name, [default]) | ref | Text input |
ui.keybind(tab, section, name, [default_vk]) | ref | "Click to bind" keybind picker. Stored as VK code. default_vk defaults to 0x06 (VK_XBUTTON2) when omitted |
ui.font_picker(tab, section, name, [default_font]) | ref | Font picker with system font search. Stored as font name string. See Draw > Fonts |
ui.button(tab, section, name, callback) | ref | Button. callback runs when pressed |
ui.label(tab, section, text) | ref | Static wrapped text label. No interactive value |
Each widget also has a ui.new_* alias (ui.new_checkbox, ui.new_slider_int, etc.). The aliases are identical to the unprefixed versions.
One exception
ui.new_label does not exist, only ui.label. Calling it errors with "attempt to call a nil value (field 'new_label')".
Reading and writing values
| Function | Returns | Description |
|---|---|---|
ui.get(ref) | varies | Current value of the widget. Also accepts (tab, sec, name) directly instead of a ref string. Type depends on the widget. nil when no widget matches |
ui.get_value(ref) | varies | Alias for ui.get |
ui.set(ref, value) | - | Set the widget's current value. Also accepts (tab, sec, name, val) directly. Each call makes the script that owns the widget save its whole config after the tick, so don't call it every frame on a persisted widget. Write once, on the frame the value settles. Values for slider_int / dropdown / keybind must be whole numbers. A fractional number stores 0. A color table needs all four of r, g, b and a. A missing one stores 0. It cannot change a multiselect |
ui.set_value(ref, value) | - | Alias for ui.set |
ui.set_visibility(ref, bool) | - | Show or hide the widget. Also accepts (tab, sec, name, visible) directly |
local mode = ui.dropdown("Misc", "General", "Mode", {"Off", "Casual", "Tryhard"}, 1)
function on_tick()
if ui.get(mode) == 3 then
-- Tryhard mode
end
end
-- Set it from on_tick or a callback. Saved values load after the top level
-- of the script runs, so a ui.set there is replaced by the saved value.
ui.button("Misc", "General", "Casual", function()
ui.set(mode, 2)
end)Use ui.set_visibility for conditional UI without re-creating widgets:
local advanced = ui.checkbox("Misc", "General", "Advanced mode")
local detail = ui.slider_int("Misc", "General", "Detail level", 1, 10, 5)
function on_tick()
ui.set_visibility(detail, ui.get(advanced))
endSections
ui.new_section creates a named card (section) inside a tab. It gives you layout options that sections created automatically by widget calls don't have.
| Function | Returns | Description |
|---|---|---|
ui.new_section(tab, ref, display, [opts]) | - | Create a section card. If tab does not exist, silently does nothing. Returns nothing. Use the ref string to name the section in later widget calls |
ui.new_container(tab, ref, display, [opts]) | - | Alias for ui.new_section |
opts keys, all optional:
| Key | Type | Default | Effect |
|---|---|---|---|
halfsize | boolean | false | Half width, so two adjacent half-size sections share a row |
responsive | boolean | false | A half-size section falls back to full width when the container is narrower than 380 px |
boxed | boolean | true | Draw the menu card (title, underline, background). boxed = false turns it off |
collapsible | boolean | false | Use the older collapsing-header look instead of the card |
icon | string | - | Preset glyph name for the panel header. Art keys don't work here. They only work on rows (ui.set_icon) |
gear | string | - | Ref of another section to show inside this panel header's cog. See ui.section_gear |
subtab | integer | -1 | Which ui.tabs page this section belongs to, 0-based. -1 keeps it visible on every page |
visible | boolean | true | Hide the whole section |
autosize and next are accepted but have no effect.
Options are applied only when the section is first created. A section auto-creates on the first widget call that names it, so ui.new_section after that call does nothing. Declare the section before its widgets.
ui.new_section("My Tab", "main_sec", "Options", { halfsize = true })
local on = ui.checkbox("My Tab", "main_sec", "Enable")Page tabs
| Function | Returns | Description |
|---|---|---|
ui.tabs(tab, labels, [icons]) | - | Add a page-level tab strip inside a script tab. It splits the tab's sections across pages. labels is a required array of strings. icons is an optional array of preset glyph names, one per label. An empty or missing entry draws no icon. It does not fall back to the cog |
Put a section on a page with the subtab option on ui.new_section. Indices are 0-based (first label = subtab = 0). A section left at the default subtab = -1 stays visible on every page, which is how you keep a common header above the strip. Call ui.tabs after the tab exists. Otherwise it silently does nothing.
ui.new_tab("my_esp", "My ESP")
ui.tabs("my_esp", { "Visuals", "Aim" }, { "eye", "crosshair" })
ui.new_section("my_esp", "status", "Status") -- on every page
ui.new_section("my_esp", "boxes", "Boxes", { subtab = 0 })
ui.new_section("my_esp", "aimbot", "Aim", { subtab = 1 })Rows
| Function | Returns | Description |
|---|---|---|
ui.begin_row(tab, section) | - | Start a row group. Every widget created until ui.end_row shares one row and gets an equal share of the section width |
ui.end_row(tab, section) | - | Close the row group opened by ui.begin_row. Widgets created after it get their own full-width rows again |
Both need an existing section and silently do nothing if it does not exist yet. Sections auto-create on their first widget call, so create the section (ui.new_section, or one widget) before calling ui.begin_row.
Rows are grouped by adjacency. Hiding a widget in the middle of a row with ui.set_visibility ends the row at that point rather than closing the gap.
ui.new_section("My Tab", "aim", "Aim")
ui.begin_row("My Tab", "aim")
ui.slider_int("My Tab", "aim", "FOV", 1, 180, 30)
ui.slider_float("My Tab", "aim", "Smoothing", 0.1, 1.0, 0.5)
ui.end_row("My Tab", "aim")Row decoration
| Function | Returns | Description |
|---|---|---|
ui.set_icon(ref, icon, [color]) | - | Put an icon on this widget's row. Also accepts (tab, sec, name, icon, [color]). icon is either a preset glyph name ("crosshair", "eye", ...) or an art key: the file name of a loaded icon such as "Infernus" or "Healing_Rite". It can also be a game image path, compiled (..._psd.vtex_c) or source (file://{images}/...), as on Game Icons. The name is looked up when the row is drawn, so an art key works even if the icon atlas is still loading when you call this. color is an optional {r, g, b} table (0-255) that tints preset glyphs only. Art icons draw untinted, and alpha is always 255 |
ui.set_sub(ref, [enabled], [inert]) | - | Indent this widget's row under the one above it, as a child setting. Also accepts (tab, sec, name, [enabled], [inert]). enabled defaults to true, so ui.set_sub(ref) turns indenting on. Pass false to turn it off. inert = true also greys the row out and blocks clicks. Consecutive sub rows share one continuous indent rail |
local on = ui.checkbox("My Tab", "aim", "Aimbot", true)
ui.set_icon(on, "crosshair")
local fov = ui.slider_int("My Tab", "aim", "FOV", 1, 180, 30)
ui.set_sub(fov) -- indented under the toggle aboveGear sections
A cog button can host an entire section, either on a widget's row or in a section's panel header.
| Function | Returns | Description |
|---|---|---|
ui.set_gear(ref, gear_section) | - | Host an entire section inside a cog button on this widget's row. Use it to tuck advanced options behind the toggle they belong to. Also accepts (tab, sec, name, gear_section). gear_section is the ref of a section in the same tab |
ui.section_gear(tab, section, gear_section) | - | Like ui.set_gear, but the cog is in the section's panel header instead of on a widget row. All three arguments are required |
The hosted section is moved, not copied, so it no longer draws in the normal section list. The move happens when ui.set_gear / ui.section_gear runs, so create the gear section (or at least its first widget) before calling either one. Otherwise it draws both inline and inside the cog. Silently does nothing if the widget or the section can't be found.
The gear option on ui.new_section only points the header cog at a section. It does not move it, so that section draws both inline and inside the cog. Use ui.section_gear, or create the hosted section with { visible = false }.
ui.new_section("My Tab", "aim_adv", "Advanced") -- declare it first...
ui.slider_float("My Tab", "aim_adv", "Smoothing", 0.1, 1.0, 0.5)
local on = ui.checkbox("My Tab", "aim", "Aimbot", true)
ui.set_gear(on, "aim_adv") -- ...then tuck it behind the cogTooltips
| Function | Returns | Description |
|---|---|---|
ui.set_tooltip(ref, text) | - | Attach hover tooltip text to a widget. Also accepts (tab, sec, name, text) directly |
ui.tooltip(ref, text) | - | Alias for ui.set_tooltip |
local spd = ui.slider_float("Aim", "General", "Speed", 0.1, 1.0, 0.5)
ui.set_tooltip(spd, "Lower = smoother. 1.0 = instant snap.")Theme and menu state
| Function | Returns | Description |
|---|---|---|
ui.accent() | Color {r,g,b,a} | Current menu accent color (matches the theme). a is always 255 |
ui.accent_color() | Color {r,g,b,a} | Alias for ui.accent |
ui.is_menu_open() | boolean | True while the overlay menu is open. ui.draggable and ui.resizable already do nothing while it is closed |
ui.menu_open() | boolean | Alias for ui.is_menu_open |
Draggable and resizable panels
Call these each tick (inside on_tick) to let users move and resize script-drawn panels while the menu is open.
| Function | Returns | Description |
|---|---|---|
ui.draggable(id, x, y, w, h) | nx, ny, dragging | Drag a panel. id must be unique per panel. Returns the (possibly updated) top-left position and a dragging flag. Save nx, ny yourself (for example with ui.set on release) |
ui.resizable(id, x, y, w, h, value, [min], [max]) | new_value, resizing, factor | Proportional resize grip (bottom-right corner). value is the size to scale. factor is the raw drag ratio. Apply it to your other sizes to scale them evenly. Call it before ui.draggable on the same panel so grabbing the grip wins over dragging, and give it a different id from that ui.draggable. Does nothing while the menu is closed |
local px = ui.slider_int("Radar", "Panel", "X", 0, 3840, 100)
local py = ui.slider_int("Radar", "Panel", "Y", 0, 2160, 100)
local ps = ui.slider_int("Radar", "Panel", "Size", 50, 800, 300)
function on_tick()
-- both helpers do nothing while the menu is closed, so the panel still draws in game
local x, y, s = ui.get(px), ui.get(py), ui.get(ps)
-- resize grip first so it wins over the body drag
local ns, resizing = ui.resizable("radar_size", x, y, s, s, s, 50, 800)
local nx, ny, dragging = ui.draggable("radar_panel", x, y, s, s)
-- ... draw the panel at nx, ny with size ns ...
-- Persist once, on the release frame: both helpers report their flag false
-- and hand back the final value on the frame the mouse comes up.
if not resizing and ns ~= s then ui.set(ps, math.floor(ns + 0.5)) end
if not dragging and (nx ~= x or ny ~= y) then
ui.set(px, math.floor(nx + 0.5))
ui.set(py, math.floor(ny + 0.5))
end
endClickable panels
ui.capture_mouse lets a panel you draw yourself take mouse clicks, for example the buttons and seek bar of a music player. Call it every tick the panel is on screen.
| Function | Returns | Description |
|---|---|---|
ui.capture_mouse(id, x, y, w, h) | hovered, pressed, down, released, mx, my | Claims the rectangle for left clicks. id is any unique string per rectangle |
hovered: the cursor is over the rectangle and clicks go to it.pressed: the left button went down on the rectangle this tick.down: the button is still held after a press that started on the rectangle. It stays true while you drag outside it, which is what a slider or seek bar needs.released: the button came up this tick, after a press that started on the rectangle.mx,my: the cursor position, in the same pixels asdraw.*.
While the cursor is over a claimed rectangle, mouse clicks go to your panel instead of the game or the menu. Right and middle clicks there are kept from the game too, but only the left button is reported. This only works while a cursor is showing: with the menu open, or when the game shows its own cursor (shop, Escape menu). While you are aiming with the mouse, hovered is false and nothing is claimed. With the menu open, a menu window on top of the rectangle wins.
function on_tick()
local x, y, w, h = 40, 400, 120, 32
local hovered, pressed = ui.capture_mouse("play_btn", x, y, w, h)
draw.rect_filled(x, y, w, h, hovered and draw.color(70, 70, 90) or draw.color(40, 40, 50), 6)
draw.text(x + 40, y + 9, "Play", draw.color(255, 255, 255))
if pressed then toast("Play pressed", 1) end
endReturn value types
| Widget | ui.get returns |
|---|---|
checkbox | boolean |
slider_int, keybind | integer |
slider_float | number |
dropdown | integer (1-based index) |
multiselect | table of booleans, indexed by option position (true = selected) |
color_picker | Color table {r, g, b, a} (0-255 per channel) |
input_text, font_picker | string |
button | nothing meaningful (use the callback) |
label | nothing meaningful (display-only) |
settings vs ui
For most scripts, declare config in the settings = { ... } table instead. It's simpler and shows up below your script in the menu automatically. You read and write it through the config.* namespace.
Use ui.* when you need custom tabs, button callbacks, or widgets you show and hide at runtime with ui.set_visibility.