Skip to content

zipsegv.net

Blog #2: XSUI updates, and also grug

Created 8/19/2025, 2:32:04 PM

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(); }
But for simple GUI apps, you probably don't actually need this much control over the initialization process, so it's kind of just boilerplate setup code. I decided to add a simple initialization mode that offers an interface somewhat like Processing:
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); }
This is much simpler and reduces the amount of boilerplate you need to place, at the cost of some control. This is most useful for GUI apps; games will most likely still want to initialize manually.
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 ... }
Now looks like this:
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); }
At first this doesn't look too different; the difference is that if we want to change the second UI, we can just call different functions on the next 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