Blog #2: XSUI updates, and also grug
Created 8/19/2025, 2:32:04 PM
Table of Contents
1. XSUI Updates
As I said in my last blog post I've been working on a UI framework called XSUI. I've worked on a bit over the last week. Mostly, I've been transitioning it to an immediate-mode approach. Previously, when I started writing it, I had mostly used retained-mode UI frameworks, and so that's what I based my library off of. Then I looked more into different ways to design UI, and I found immediate mode style UI frameworks, which to me seem a lot more flexible and easy to use.1.1. Simple Setup
I haven't added too much in terms of actual features yet (completely changing the public API of a library is a lot of work, actually) but I have added a nice QoL improvement: simple setup.Before, you had to manually initialize your backend and a lot of XSTD stuff before even being able to use XSUI (example below with SDL2):
int main(void) { // init sdl ASSERT(SDL_Init(SDL_INIT_VIDEO) == 0); ASSERT(TTF_Init() == 0); SDL_Window *win = SDL_CreateWindow("XSTD UI Test", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1200, 800, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); ASSERT(win != NULL); SDL_Renderer *rend = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED); ASSERT(rend != NULL); TTF_Font *font = TTF_OpenFont("./test_assets/LiberationSans.ttf", 32); ASSERT(font != NULL); // init xstd xstd_allocator_t *ally = xstd_c_allocator(); // init xsre xsre_ctx_t *xrend = xsre_sdl2_ctx(rend); xsre_font_t *xfont = xsre_sdl2_font(font); xsre_listener_list_t *list = xsre_listener_list_new(ally); // init xsui xsui_state_t *uistate = xsui_state_new(ally, list, xrend, xfont, XSUI_DARK_THEME); // SDL event loop bool button_has_been_clicked = false; while(true) { SDL_Event e; while(SDL_PollEvent(&e)) { xsre_sdl2_send_event(list, &e); switch(e.type) { case SDL_QUIT: goto quit; default: break; } } SDL_SetRenderDrawColor(rend, 0, 0, 0, 0); SDL_RenderClear(rend); // UI rendering code goes here SDL_RenderPresent(rend); } quit: xsui_state_destroy(uistate); xsre_ctx_destroy(xrend); xsre_listener_list_destroy(list); xstd_allocator_destroy(ally); TTF_CloseFont(font); SDL_DestroyRenderer(rend); SDL_DestroyWindow(win); SDL_Quit(); }
void settings(xsui_setup_settings_t *settings, void *data) { // code to set some initial settings goes here settings->font_path = "./test_assets/LiberationSans.ttf"; settings->font_size = 32; } void setup(xsui_state_t *ui, xstd_allocator_t *ally, void *data) { // Post-initialization setup code goes here } void draw(xsui_state_t *ui, xstd_allocator_t *ally, void *data) { // UI rendering code goes here } int main(void) { return xsui_setup(NULL, &settings, &setup, &draw); }
You can, of course, still get your backend's underlying renderer by calling
xsui_state_get_ctx(ui)
and then using a backend-specific function to cast that to a renderer to be used with your backend.1.2. Immediate Mode
Immediate Mode means that you specify the whole UI hierarchy every frame. Internally, XSUI keeps a retained-mode-style tree, and modifies it whenever it detects that you have specified a different tree than the last frame. So, what previously looked like this with my retained mode approach:static void on_button_pressed(xsui_frame_t *frame, void *data) { printf("I have been clicked!"); } int main(void) { // ... initialization code ... // Note: The "vbox" string below is a unique ID for the frame. Unique IDs are // something I decided to remove when moving to immediate mode. xsui_frame_t *vbox = xsui_box_new(ui, NULL, STRING("vbox"), 0); xsui_frame_set_desired_pos(vbox, 50, 50); xsui_frame_set_desired_size(vbox, 500, 0); xsui_label_new(ui, vbox, STRING("label"), STRING("Hello, World!")); xsui_button_new(ui, vbox, STRING("button"), STRING("Click me!"), &on_button_pressed, NULL, NULL); xsui_frame_t *button2 = xsui_button_new(ui, vbox, STRING("button2"), STRING("this is a long button label"), NULL, NULL, NULL); xsui_frame_set_color(button2, XSUI_COLOR_SECONDARY); xsui_frame_t *hbox = xsui_box_new(ui, NULL, STRING("hbox"), XSUI_BOX_HORIZONTAL | XSUI_BOX_NO_BORDER); xsui_button_new(ui, hbox, STRING("foo_button"), STRING("Foo"), NULL, NULL, NULL); xsui_button_new(ui, hbox, STRING("bar_button"), STRING("Bar"), NULL, NULL, NULL); xsui_button_new(ui, hbox, STRING("baz_button"), STRING("Baz"), NULL, NULL, NULL); // end // end while(true) { // ... event handling code ... xsui_state_render_root(ui); } // ... cleanup code ... }
void draw(xsui_state_t *ui, xstd_allocator_t *ally, void *data) { xsui_begin_tree(ui); xsui_box(ui, 0); // note: "frames" (what xsui calls "components" or "widgets") still exist; but the // UI tree itself is managed by XSUI but not the user. So to set the property of a // frame, we need to actually grab the frame with `xsui_last_frame()`. // this is different from libraries like Dear ImGUI which do not expose their UI // tree at all. xsui_frame_set_desired_pos(xsui_last_frame(ui), 50, 50); xsui_frame_set_desired_size(xsui_last_frame(ui), 500, 0); xsui_label(ui, STRING("Hello, World!")); if(xsui_button(ui, STRING("Click me!"))) printf("I have been clicked!"); xsui_button(ui, STRING("this is a long button label")); xsui_frame_set_color(xsui_last_frame(ui), XSUI_COLOR_SECONDARY); xsui_box(ui, XSUI_BOX_HORIZONTAL | XSUI_BOX_NO_BORDER); xsui_box_margin(xsui_last_frame(ui), 0); xsui_button(ui, STRING("Foo")); xsui_button(ui, STRING("Bar")); xsui_button(ui, STRING("Baz")); xsui_end_container(ui); xsui_end_container(ui); xsui_end_tree(ui); }
draw()
call, then we just let XSUI modify the tree for us. If we wanted to change the first, we'd have to carefully manipulate the UI tree and keep references to all the frames we want to manipulate. Using immediate mode does have some drawbacks, however. I am going to have to sacrifice some of the design goals of having XSUI be "user-modifiable". I think I'm still going to try to keep some of that philosophy but it's going to be a lot harder when even the structure of the UI is defined in code. I think I have a solution to this, but it's going to take some tweaking to get right, and it's probably a far-off goal.
2. grug: An interesting little modding language/framework
Recently, I discovered grug, an interesting little modding language. I recommend you watch Trez's youtube video about it, but in short it's a modding language that tries to be as simple and as easy to integrate as possible. To this end, the language supports a very small amount of types, and directly integrates with C function calls with very little binding necessary.One of the notable things about grug is its complete lack of builtin collection types; games are expected to provide their own collections to grug instead. This generally simplifies the language and implementation a lot and makes memory ownership much simpler. It also makes binding simpler as you don't need to convert between types from the host language and types from the mod language.
Another interesting design decision is that all public API functions and types (grug is statically typed) are specified in a single
mod_api.json
, which is easily parseable and can be used to generate documentation. I've been contributing a bit to the language and proposing ideas. In addition, I've been writing some documentation for the language too (but it's currently heavily WIP). I also have a game I'm working on (that I'll announce once it's in a playable state) that I might try to add grug modding support to.
Groups: Blog