Screens and menus
Lesson 9.3
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
static void hello_done(cl_widget w, void* user) {
(void)w;
cl_screen_close(*(cl_screen*)user);
}
static cl_screen g_hello;
static void hello_init(cl_screen s, void* user) {
(void)user;
int32_t cx = cl_screen_width(s) / 2;
cl_widget_label(s, cx, 40, CL_STR("This is my first screen."), 0xFFFFFFFF, true, NULL);
cl_widget_button(s, cx - 100, cl_screen_height(s) - 40, 200, 20, CL_STR("Done"), hello_done, &g_hello, NULL);
}
static void open_hello(void) {
cl_screen_desc d = CL_SCREEN_DESC_INIT;
d.title = CL_STR("Hello");
d.pause = false;
d.init = hello_init;
cl_screen_open(&d, &g_hello);
}
Open it from client code, for example on a keybind (see Keys and client events):
while (cl_keybind_consume_click(g_open_key)) open_hello();
The screen descriptor:
| 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, user) |
Build the widgets. | |
render(s, g, mouse_x, mouse_y, partial_tick, user) |
Draw extra things every frame. | |
tick(s, user) |
Runs 20 times a second while open. | |
key(s, key, scancode, modifiers, user) |
A key was pressed. Return true if you used it. |
|
char_typed, mouse, scroll |
Typed characters, mouse clicks and the wheel. Return true if you used them. |
|
close(s, user) |
The screen closed. | |
user |
NULL |
Your pointer, passed to every function above. |
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 cl_screen_width(s) and cl_screen_height(s); 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 the last parameter receives a cl_widget handle if you want to change it later (NULL if not).
static int g_volume = 50;
static bool g_sparkles = true;
static char g_name[33] = "";
static void on_click(cl_widget w, void* user) {
(void)user;
cl_widget_set_label(w, CL_STR("Clicked!"));
}
static void on_sparkles(cl_widget w, void* user) {
(void)user;
g_sparkles = cl_widget_value(w) == 1.0;
}
static void on_volume(cl_widget w, void* user) {
(void)user;
g_volume = (int)cl_widget_value(w);
}
static void on_name(cl_widget w, void* user) {
(void)user;
size_t len = 0;
cl_widget_text(w, g_name, sizeof g_name, &len);
}
static void settings_init(cl_screen s, void* user) {
(void)user;
int32_t x = cl_screen_width(s) / 2 - 100;
cl_widget_button(s, x, 50, 200, 20, CL_STR("Click me"), on_click, NULL, NULL);
cl_widget_checkbox(s, x, 80, CL_STR("Show sparkles"), g_sparkles, on_sparkles, NULL, NULL);
cl_widget_slider(s, x, 110, 200, 20, CL_STR("Volume"), 0, 100, g_volume, 5, on_volume, NULL, NULL);
cl_widget_text_field(s, x, 140, 200, 20, CL_STR("Your name"), 32, on_name, NULL, NULL);
}
| Widget | Its callback reads |
|---|---|
cl_widget_button(s, x, y, w, h, label, on_press, user, &out) |
|
cl_widget_checkbox(s, x, y, label, checked, on_change, user, &out) |
cl_widget_value(w) is 1 or 0 |
cl_widget_slider(s, x, y, w, h, label, min, max, value, step, on_change, user, &out) |
cl_widget_value(w) is the number; step snaps it, 0 for smooth |
cl_widget_text_field(s, x, y, w, h, hint, max_length, on_change, user, &out) |
cl_widget_text(w, buf, cap, &len); hint shows while empty |
cl_widget_label(s, x, y, text, argb, centered, &out) |
(no callback) |
A slider shows its value in the label, like Volume: 50. Labels are plain text, not translation keys.
Every widget can be changed later through its handle: cl_widget_set_label, cl_widget_set_tooltip, cl_widget_set_active(w, false) (greyed out), cl_widget_set_visible, cl_widget_set_value, cl_widget_set_text, cl_widget_set_bounds and cl_widget_remove.
Keep state outside the screen
The widgets are rebuilt on every resize, so keep the values in static variables (like g_volume above) and read them when building the widgets. Then a resize never loses what the player chose.
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:
static void settings_render(cl_screen s, cl_gui g, int32_t mx, int32_t my, float partial_tick, void* user) {
(void)mx;
(void)my;
(void)partial_tick;
(void)user;
cl_gui_text_centered(g, CL_STR("My Mod"), cl_screen_width(s) / 2, 20, 0xFFFFFFFF, true);
cl_gui_item_by_name(g, CL_STR("my_mod:ruby"), cl_screen_width(s) / 2 - 8, 170);
}
Bigger layouts: RML documents
For screens with a lot of content, you can write the layout as a document, similar to HTML and CSS, in assets/my_mod/gui/shop.rml, and open it with cl_rml_open. Your code fills in values with cl_rml_set_text(doc, id, text) and gets clicks in the descriptor’s on_event:
static void shop_event(cl_rml_doc doc, cl_str element_id, cl_str event, cl_str value, void* user) {
(void)doc;
(void)value;
(void)user;
CL_LOG("%.*s on %.*s", (int)event.len, event.ptr, (int)element_id.len, element_id.ptr);
}
static void open_shop(void) {
cl_rml_desc d = CL_RML_DESC_INIT;
d.path = CL_STR("assets/my_mod/gui/shop.rml");
d.on_event = shop_event;
cl_rml_doc doc;
if (cl_rml_open(&d, &doc) == CL_OK) {
cl_rml_set_text(doc, CL_STR("balance"), CL_STR("12 rubies"));
}
}
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.
static bool shop_click(cl_menu m, cl_entity who, int32_t slot, int32_t button, cl_click_type click, void* user) {
(void)m;
(void)button;
(void)click;
(void)user;
if (slot >= 0 && slot < 9) {
cl_easy_give(who, "my_mod:ruby", 1);
cl_easy_send_action_bar(who, "+1 ruby");
}
return false; // don't let vanilla move the item
}
static void open_shop_menu(cl_entity player) {
cl_menu_desc d = CL_MENU_DESC_INIT;
d.kind = CL_MENU_CHEST_9X1;
d.title = cl_text_literal(CL_STR("Ruby Shop"));
d.on_click = shop_click;
cl_menu shop;
cl_result r = cl_menu_open(player, &d, &shop);
cl_text_release(d.title);
if (r != CL_OK) return;
cl_item_stack ruby = cl_stack_create_by_name(CL_STR("my_mod:ruby"), 1);
for (int32_t slot = 0; slot < 9; ++slot) cl_menu_set_item(shop, slot, ruby);
cl_stack_release(ruby);
}
Open it from server code, for example in a command (open_shop_menu(cl_cmd_ctx_player(ctx))).
kindis the size:CL_MENU_CHEST_9X1up toCL_MENU_CHEST_9X6,CL_MENU_DISPENSER_3X3orCL_MENU_HOPPER.titleis acl_text. The menu copies it, so release yours aftercl_menu_open.on_click(menu, player, slot, button, click, user)runs for every click. Slot numbers start at 0 for your slots, then come the player’s inventory slots;-999is a click outside the window. Returntrueto let the click happen normally,falseto cancel it.on_close(menu, player, user)runs when it closes.- Menus are locked by default: nothing can be taken out or put in unless
on_clickreturnstrue.d.locked = falseturns that off.
cl_menu_set_item copies the stack into the slot, so one stack can fill all nine. cl_menu_get_item(m, slot) gives you an owned copy back, cl_menu_size(m) the number of slots, and cl_menu_close(m) closes it.
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
#include <clight_easy.h>
#include <stdlib.h>
static int g_size = 6;
static void size_changed(cl_widget w, void* user) {
(void)user;
g_size = (int)cl_widget_value(w);
}
static void roll_pressed(cl_widget w, void* user) {
(void)user;
char label[32];
snprintf(label, sizeof label, "Rolled %d", 1 + rand() % g_size);
cl_widget_set_label(w, CL_STR_C(label));
}
static void roller_init(cl_screen s, void* user) {
(void)user;
int32_t x = cl_screen_width(s) / 2 - 100;
cl_widget_slider(s, x, 60, 200, 20, CL_STR("Size"), 1, 10, g_size, 1, size_changed, NULL, NULL);
cl_widget_button(s, x, 90, 200, 20, CL_STR("Roll"), roll_pressed, NULL, NULL);
}
static void open_roller(void) {
cl_screen_desc d = CL_SCREEN_DESC_INIT;
d.title = CL_STR("Roller");
d.pause = false;
d.init = roller_init;
cl_screen_open(&d, NULL);
}
step = 1 makes the slider jump between whole numbers. g_size lives outside the screen, so the slider keeps its position when the window is resized.
Recap: cl_screen_open with a cl_screen_desc builds a client screen whose widgets are created in init (again on every resize); cl_menu_open with a cl_menu_desc opens a server-side chest menu that works for every player.