Leaving IMGUI Behind

After a solid attempt to use immediate mode GUI concepts in my programs, I'm giving up.

A naive implementation of an IMGUI is straightforward, easy to integrate, and easy to use. Unfortunately, when this naive implementation meets the big bad world of production use, it grows up fast.

The strength of IMGUI is that it doesn't care how you use it. You need to throw in a debug UI that adjusts a value here, pokes a string there, and it works great.

I'll briefly summarize my use and abuse of my homegrown IMGUI library, starting with a dirt-simple example:

void my_game_loop(double dt) {  
    if (imgui_button(400, 300, "Spawn Enemy")) {
        spawn_enemy();
    }
}

But if you're trying to construct a dynamic game UI with layout, what then? You define some sort of active area (which sets hidden state):

void my_game_update(double dt) {  
    imgui_begin(600, 400, imgui_layout_flow_lr_tb);
    if (imgui_button("Spawn Enemy")) {
        spawn_enemy();
    }
}

And you want it to do its processing in the engine phase of your loop, and its drawing in the render phase of your loop, so you have it manage a display list (which sets hidden state):

void my_game_engine(double dt) {  
    imgui_begin(600, 400, imgui_layout_flow_lr_tb);
    if (imgui_button("Spawn Enemy")) {
        spawn_enemy(current_enemy);
    }
    imgui_end();
}

void my_game_render(void) {  
    imgui_render();
}

And you want to integrate it with other rendering, but you have to set it up in the engine phase, so you write a callback "control" (which sets hidden state) that escapes the IMGUI display list and runs your special rendering function:

void draw_enemy_preview(rect2d draw_area, void *context) {  
    enemy_t *current_enemy = context;
    ...
}

void my_game_engine(double dt) {  
    imgui_begin(600, 400, imgui_layout_flow_lr_tb);
    imgui_draw_callback(draw_enemy_preview, vec2d_set(100, 100), current_enemy);
    if (imgui_button("Spawn Enemy")) {
        spawn_enemy(current_enemy);
    }
    imgui_end();
}

void my_game_render(void) {  
    imgui_render();
}

If you're following along at home, you can see that by the time you have a full featured UI, you have a lot of hidden state inside the IMGUI context.

So IMGUI is no simpler to implement well (and nobody seems to be claiming it is). But is it simpler to use?

It's very explicit, and many programmers like that. You can see what is drawn, and when. Data binding can be just as explicit and easy: a slider control could take a pointer to a float and modify the value directly.

But any sane UI implementation, including IMGUI, needs to retain state. It needs to render and cache text. It needs to know where it last put a control so it can put the next in the right spot. It needs to store skin parameters like fonts and colours, if those are configurable. It needs to know, from frame to frame, what state it can keep and what it can discard, which means that the API must accept that state every frame and check it against the last state every frame.

We are now left with an "IMGUI" wrapper around a retained-mode UI. In other words, we have a retained UI in which the layout data is encoded as function parameters, and the layout must be re-evaluated every frame.

IMGUI is a solution to the problem of UIs that hide an unreasonable amount of complexity for what they do. If you make an IMGUI try to do too much, you get the same complexity, only it's not hidden. It's in all your code that calls the IMGUI.

You can roll a decent UI from scratch. I will attest to this; I've done it twice now.

When

  • you need that UI in a hurry
  • it's strictly for debugging
  • it's about developer convenience

go IMGUI and regret nothing.

When

  • you need that UI to be user-facing;
  • you need it to be reusable;
  • you need to be able to serialize it;
  • you can anticipate being frustrated at a lack of separation of concerns and having to write MVC wrappers,

bite the bullet and acknowledge that the retained model fits your problem better.

That's the genesis of RUI, a C99 UI library I'm currently whipping into shape, and I'll talk more about it when maturity warrants.