Screens and menus
Lesson 14.4
A screen is a full window over the game, like the options menu: the mouse is free and players click buttons. Mods can build their own screens in the client, or open chest-style menus from the server.
Opening a screen
local function open_hello()
cl.screen.open{
title = "Hello",
pause = false,
init = function(s)
local cx = s:width() // 2
s:label(cx, 40, "This is my first screen.", 0xFFFFFF, true)
s:button(cx - 100, s:height() - 40, 200, 20, "Done", function()
s:close()
end)
end,
}
end
Open it from client code, for example on a keybind (see Keys and client events):
cl.on("tick.client_post", function()
while key:consume_click() do open_hello() end
end)
The screen description:
| Field | Default | Means |
|---|---|---|
title |
A translation key, or plain text. Read out by the narrator. | |
pause |
true |
Pause a singleplayer game while open. |
background |
true |
Darken the world behind it. |
close_on_esc |
true |
Esc closes it. |
init(s) |
Build the widgets. | |
render(s, g, mouse_x, mouse_y) |
Draw extra things every frame. | |
tick(s) |
Runs 20 times a second while open. | |
key(s, key, scancode, modifiers) |
A key was pressed. Return true if you used it. |
|
close(s) |
The screen closed. |
init runs when the screen opens and again whenever the window is resized, after the old widgets were removed. So build all widgets in init, and place them from s:width() and s:height(); then they move to the right place on every resize.
Widgets
Widgets look and behave like vanilla’s. Each is created on the screen s and returns a widget object.
init = function(s)
local cx = s:width() // 2
s:button(cx - 100, 50, 200, 20, "Click me", function(w)
w:set_label("Clicked!")
end)
s:checkbox(cx - 100, 80, "Show sparkles", true, function(w)
sparkles = w:value() == 1
end)
s:slider(cx - 100, 110, 200, 20, "Volume", 0, 100, 50, 5, function(w)
volume = w:value()
end)
s:text_field(cx - 100, 140, 200, 20, "Your name", 32, function(w)
name = w:text()
end)
end
| Widget | Arguments | Its function gets |
|---|---|---|
s:button(x, y, w, h, label, on_press) |
the button | |
s:checkbox(x, y, label, checked, on_change) |
the checkbox; w:value() is 1 or 0 |
|
s:slider(x, y, w, h, label, min, max, value, step, on_change) |
step snaps the value, 0 for smooth |
the slider; w:value() is the number |
s:text_field(x, y, w, h, hint, max_length, on_change) |
hint shows while empty |
the field; w:text() is the text |
s:label(x, y, text, colour, centered) |
(no function) |
A slider shows its value in the label, like Volume: 50. Labels are plain text, not translation keys.
Every widget can be changed later: set_label, set_tooltip, set_active(false) (greyed out), set_visible(false), set_value, set_text and remove().
Drawing on a screen
render gets the same drawing context as a HUD layer (see The HUD), plus the mouse position. It is called after the background and before the widgets:
render = function(s, g, mx, my)
g:text_centered("My Mod", s:width() // 2, 20, 0xFFFFFF, true)
g:item_by_name("my_mod:ruby", s:width() // 2 - 8, 160)
end
Remember values outside the screen
The screen is rebuilt on every resize, so keep its state in variables outside it, and read them when building the widgets:
local volume = 50
local function open_settings()
cl.screen.open{
title = "Settings",
init = function(s)
s:slider(s:width() // 2 - 100, 60, 200, 20, "Volume", 0, 100, volume, 5, function(w)
volume = w:value()
end)
end,
}
end
Bigger layouts: RML documents
For screens with a lot of content, you can write the layout as a document, similar to HTML and CSS, and open it with cl.rml.open{ path = "assets/my_mod/gui/shop.rml" }. Your script fills in values with doc:set_text(id, text) and gets clicks in on_event. The reference docs cover the format; for most mods, the widgets above are enough.
Menus from the server
A menu is a chest-style window that the server opens for a player: item slots instead of buttons. It uses vanilla’s chest menus, so it even works for players without your mod. That makes it the right tool for shops, kits and choices.
local function open_shop(player)
local shop = cl.menu.open(player, {
kind = "chest_9x1",
title = "Ruby Shop",
on_click = function(menu, who, slot)
if slot >= 0 and slot < 9 then
who:give("my_mod:ruby")
who:send_action_bar("+1 ruby")
end
return false -- don't let vanilla move the item
end,
})
for slot = 0, 8 do
shop:set_item(slot, "my_mod:ruby")
end
end
Open it from server code, for example in a command (open_shop(ctx:player())).
kindis the size:"chest_9x1"up to"chest_9x6","dispenser_3x3"or"hopper".on_click(menu, player, slot, button, click)runs for every click. The slot numbers start at 0 for your slots, then come the player’s inventory slots. Returntrueto let the click happen normally,falseto cancel it.on_close(menu, player)runs when it closes.- Menus are locked by default: nothing can be taken out or put in unless
on_clickreturnstrue.locked = falseturns that off.
menu:set_item(slot, item) puts an item into a slot (an id or a stack), menu:get_item(slot) reads it, and menu:close() closes the menu.
Try it
Make a client screen with a slider “Size” from 1 to 10 and a button “Roll”. Pressing the button sets its own label to a random number between 1 and the slider’s value.
Show a solution
local size = 6
local function open_roller()
cl.screen.open{
title = "Roller",
pause = false,
init = function(s)
local cx = s:width() // 2
s:slider(cx - 100, 60, 200, 20, "Size", 1, 10, size, 1, function(w)
size = math.floor(w:value())
end)
s:button(cx - 100, 90, 200, 20, "Roll", function(w)
w:set_label("Rolled " .. math.random(1, size))
end)
end,
}
end
step = 1 makes the slider jump between whole numbers. math.floor turns the value into an integer for math.random.
Recap: cl.screen.open{ title, init, render } builds a client screen with vanilla widgets, rebuilt in init on every resize; cl.menu.open(player, { kind, on_click }) opens a server-side chest menu.