menu tabs, style and surfaces
Add a tab of your own to the menu, give the whole menu a look, and keep live drawing panels in a tab, on a page or floating on the screen.
On this page
A script can do more than add a row to the scripts page. It can open a tab in the menu bar, fill it with groups and with a live drawing panel, set the accent, text colour, tint and rounding of the whole menu, and keep a small floating panel on the screen while the menu is open. The menu draws every control and every frame around a panel. The script reads plain values and draws into the rectangle it is given.
Groups, rows and the control kinds are on the ui page. This page covers the three things that sit above them.
What you can add
| call | what it gives you |
|---|---|
| ui.tab | a tab in the menu bar, with two columns of groups |
| ui.style | a look for the whole menu: colours, rounding, gaps |
| ui.surface | a rectangle of the menu that the script draws into, with a pointer |
| ui.is_open | whether the menu is open |
Two switches on the scripts page belong to the user: show script groups in pages for all scripts, and show in pages for one script. When either is off, the script's tabs and panels are not shown and its tab groups stay on the scripts page, so every value stays reachable.
Functions
ui.tab
ui.tab(name) -> tab object
| argument | type | meaning |
|---|---|---|
name | string | the name on the tab bar. At most 12 bytes, longer text is cut. It is cleaned like every label, and blanks around it do not count. |
Returns a tab object. The same name gives the same tab.
The tab appears after configs in the tab bar, marked with a dot in front of its name, for example . radar, in the order the scripts were loaded. The names of Starlit's own tabs (rage, legit, visuals, misc, skins, configs) and of the pages behind configs (scripts, keybinds) are taken, and so is a name that another running script already uses. A script may have 2 tabs and all running scripts together 3, which is what the tab bar has room for.
The tab page has two columns like the visuals page. Groups and surfaces are listed by order, then by script, then by creation, and "auto" alternates left and right. A tab that goes away while it is open, because the script is unloaded, reloaded, stopped or hides it, takes the user back to configs.
local t = ui.tab("radar")
local g = t:group("radar", { column = "left", order = 1 })
g:checkbox("enabled", true)
t:group
t:group(name [, { column = "auto" | "left" | "right", order = -100 to 100 }]) -> group object
Returns a group object like ui.group does, with the same controls and options, and the same 12 groups per script. The header of the group reads name - file. A group lives in one place: a group of the same name that was on a page moves into the tab, and ui.group(name, { page = ... }) moves it back.
t:visible
t:visible(b)
false takes the tab out of the bar while its groups keep working. true brings it back.
ui.style
ui.style{ key = value, ... }
ui.style()
Gives the menu a look. Call it again to replace what the script asked before. ui.style() with no table takes the look back. There is no getter.
| key | value | meaning |
|---|---|---|
accent | colour | the slider fill, merged captions and key capture. The dark end of the sliders follows at 55 percent. |
accent_dark | colour | the dark end of the slider fill, when you want your own |
text, text_dim | colour | labels and secondary text |
tint | colour | the colour of the window. The group boxes, headers and inputs move with it. |
rounding | number 0 to 6 | corner radius of the window, the group boxes, the controls and the checkboxes |
row_gap | number 2 to 8 | the space between rows |
header_spacing | number 0 to 3 | letter spacing of the group headers |
tab_underline | boolean | a line under the active tab |
checkbox_ticked | boolean | checkboxes with a tick |
A colour is a color(r, g, b) object or a table with numeric r, g and b. The alpha is always 255. A number outside its range is cut to it, and NaN or infinity raises an error. Keys that do not exist are ignored.
Readable text. The menu keeps its text readable. Text against the window needs a lightness difference of 0.45, dim text 0.25, the accent against a group box 0.25, and the white titles against their strip 0.30. A value that would miss one of these is replaced by the user's preset value, and the tint goes last. Presets and the HUD overlays are not touched.
Lifetime. The look lasts while the script runs and keeps asking. It is gone when the script is unloaded, stops, raises an error, or the level changes, so ask again from events.on_level. With several scripts, the one loaded last wins as a whole.
The user. The scripts page shows style: file and a button reset menu style. It puts the preset back and ignores the styles of scripts until the user presses allow script styles.
ui.style{ accent = color(255, 170, 60), rounding = 2, tab_underline = true }
events.on_level(function()
ui.style{ accent = color(255, 170, 60), rounding = 2, tab_underline = true }
end)
ui.surface
ui.surface(name [, options]) -> surface object
A surface is a rectangle of the menu that the script draws into. There are four ways to place it.
ui.surface("radar", { tab = t, column = "right", order = 1, w = 260, h = 260 }) -- in a script tab
ui.surface("log", { page = "misc", column = "right", order = 5, w = 300, h = 120 }) -- in a page of the menu
ui.surface("info", { floating = true, x = 30, y = 200, w = 190, h = 64, title = "info" }) -- a panel of its own
ui.surface("plain") -- the scripts page
| option | values |
|---|---|
tab, page, floating | one of the three, because a surface lives in one place. page is "scripts", "rage", "legit", "visuals" or "misc". With none of them the surface goes on the scripts page. |
column, order | as for groups: "auto", "left" or "right", and -100 to 100 |
w, h | the size you would like, 32 to 900 wide and 32 to 700 high. A column can be narrower, and the callback is told the size it really has. |
wheel | true gives the mouse wheel over the surface to the script, so it does not scroll the page. Default false. |
x, y, title | floating only: the first position and the title. The user drags the panel by its title bar, and it stays on the screen. |
The name is at most 32 bytes, and the same name gives the same surface. A script may have 2 surfaces. The menu draws a caption with the script's name over a mounted surface, a title bar over a floating one, and a 1 pixel frame around both.
s:on_draw
s:on_draw(function(w, h, m) ... end)
s:on_draw(nil)
Sets the function that draws the surface, or removes it with nil. It runs once per frame, before the menu is drawn, and only for a surface the menu showed in the last frame: the menu is open, the page or tab is the one the user is on, and the switches are on. It has its own budget of 50,000 instructions and 0.5 ms. An error or an overrun counts as a strike like in any callback.
w and h are the size the surface really has. All draw functions work inside it, in the surface's own coordinates: (0, 0) is the top left corner and w, h is the bottom right. The 2D calls are available (rect, line, circle, text, polyline, gradient, clip and the others), and what is drawn is cut to the rectangle. The surface has its own buffer of 1000 commands, 8 KiB of text and 1024 point values.
m is nil unless the pointer is over the surface or a press started on it. Otherwise it is a table:
| field | meaning |
|---|---|
mx, my | the pointer in surface pixels |
down | the left button is down |
pressed, released | the left button went down or up in this frame |
wheel | the wheel steps, -3 to 3, when wheel = true was asked for |
A press that starts on the surface belongs to the script until the button is released, and mx, my stay inside the rectangle meanwhile. A surface sees the left button and, if you asked for it, the wheel. The right button, keys and text stay with the menu.
s:size
s:size(w, h)
Changes the size you would like, within the same 32 to 900 and 32 to 700.
s:visible
s:visible(b)
false hides the surface and true shows it again.
ui.is_open
ui.is_open() -> boolean
Returns true while the menu is open. Use it to draw something extra only when the menu is closed, or only when it is open.
Example: a tab with a live radar
A tab called radar. The groups sit on the left, the radar on the right. While the pointer is over the radar, a crosshair follows it and the distance to the middle is printed in metres.
local tab = ui.tab("radar")
local main = tab:group("radar", { column = "left", order = 1 })
local on = main:checkbox("enabled", true)
local range = main:slider("range (m)", 10, 200, 80, { tooltip = "enemies farther away are not shown" })
local look = tab:group("look", { column = "left", order = 2 })
local tint = look:color("enemy color", 255, 90, 90)
local grid = look:checkbox("grid", true, { same_line = true })
look:text("move the pointer over the radar to read a distance", "dim")
local panel = ui.surface("radar", { tab = tab, column = "right", order = 1, w = 260, h = 260 })
panel:on_draw(function(w, h, m)
draw.rect_filled(0, 0, w, h, 14, 14, 18, 255)
if grid:get() then
draw.line(w / 2, 0, w / 2, h, 60, 60, 70, 255)
draw.line(0, h / 2, w, h / 2, 60, 60, 70, 255)
end
local me = entity.local_player()
local scale = (w / 2) / (range:get() / 0.0254) -- game units to pixels
if on:get() and me then
local mx, my = me:origin()
local r, g, b = tint:get()
for _, p in ipairs(entity.players(true)) do
local x, y = p:origin()
if x then
local dx, dy = (x - mx) * scale, (my - y) * scale
if math.abs(dx) < w / 2 and math.abs(dy) < h / 2 then
draw.circle_filled(w / 2 + dx, h / 2 + dy, 3, r, g, b, 255)
end
end
end
end
if m then -- only while the pointer is over the panel
draw.line(m.mx - 4, m.my, m.mx + 4, m.my, 255, 255, 255, 255)
draw.line(m.mx, m.my - 4, m.mx, m.my + 4, 255, 255, 255, 255)
local metres = math.sqrt((m.mx - w / 2) ^ 2 + (m.my - h / 2) ^ 2) / scale * 0.0254
draw.text(6, h - 18, string.format("%.0f m", metres), 255, 255, 255, 255, "small")
end
end)
Example: a floating panel and a look
A small panel that floats on the screen, counts rounds and clicks, and a warm accent for the whole menu.
ui.style{ accent = color(255, 170, 60), rounding = 2, tab_underline = true }
local rounds = 0
events.on_round_start(function() rounds = rounds + 1 end)
local clicks = 0
local panel = ui.surface("session", { floating = true, x = 30, y = 200, w = 190, h = 64, title = "session" })
panel:on_draw(function(w, h, m)
draw.rect_filled(0, 0, w, h, 16, 16, 20, 255)
draw.text(8, 8, string.format("rounds seen %d", rounds), 230, 230, 235, 255)
draw.text(8, 26, string.format("up %.0f s", time.now()), 160, 160, 170, 255)
if m and m.pressed then clicks = clicks + 1 end
draw.text(8, 44, string.format("clicks %d", clicks), 160, 160, 170, 255)
end)