Tools Development and The Need For GUI Code

December 24, 2018 —

As some of the "foundational" pieces of code have been coming together, I've realized that I need to get some tools created to assist me in content creation. This would be for things from asset conversion to tilemap editors.

Asset conversion tools (e.g. going from BMP/PCX to a game-engine specific format optimized for faster loading) can really just be command-line driven which makes it simple enough to create. For more involved things such as a tilemap editor, this requires some GUI code.

I made a few tools in straight DOS using QBasic back in 1998/1999 or so with my own homebrew GUI code that was pretty terrible as I recall. But it was my very first experience writing such code (and only a few years after I first learned to program), so perhaps I should not be too hard on myself. I actually want to add a "projects" section to this website in the near future where I can post that old code just for fun, but currently I don't have it in a presentable format to share for this post.

Very soon after, I moved to Visual Basic (first v4, and then v6) and of course, that made writing such tools infinitely easier. I was actually pretty happy with the tilemap editor I wrote (from scratch entirely in a weekend):

For my current DOS projects, I once again need to revisit GUI code 20 years later. There are some libraries I could use of course, but I am going the "Do It Yourself" route (a.k.a. the "Not Invented Here Syndrome" route) for fun and for the (re-)learning experience. With that in mind, I decided I wanted to pursue an Immediate Mode GUI implementation. Probably the most well known implementation of this these days is the Dear ImGui library.

I began reading up on it in more detail, having not implemented such a system before in any project I'd worked on. This video in particular is a great introduction to it (and I believe, the original video on it) if you're not too familiar with why this method of implementing a GUI might be useful and where/when. As well, I found this tutorial series to be quite useful.

Eventually I did get something working myself:

It's far from perfect, but I'm happy with how it's coming along.

while not KeyState[KeyEsc] do begin
    event := PollEvents;
    if event <> nil then UIProcessEvent(event);

    Cls(9);
    UIBeginFrame;

    UIBeginMenuBar;
    if UIBeginMenu('File', 100) then begin
        UIMenuItem('New');
        UIMenuItem('Open');
        UIMenuItem('Save');
        UIMenuItem('Save As');
        UIMenuItem('Quit');
        UIEndMenu;
    end;
    if UIBeginMenu('Edit', 100) then begin
        UIMenuItem('Cut');
        UIMenuItem('Copy');
        UIMenuItem('Past');
        UIMenuItem('Clear');
    end;
    if UIBeginMenu('Help', 100) then begin
        UIMenuItem('About');
    end;
    UIEndMenuBar;

    UIBeginWindow(50, 30, 250, 180, 'window!');

    if UIButton('button 1') then
        clicked := true;
    if UIButton('button 2') then
        break;

    UISetNextID('vertSlider');
    UIVertScrollBar('', 20, 0, 2, value);
    UIHorizScrollBar('slider!', 0, 0, 200, value2);

    UICheckBox('checkbox', checked);
    UIListBox('list!', 0, 3, list, listScrollY, listSelection);

    if UIRadioButton('one', (radio = 1)) then radio := 1;
    if UIRadioButton('two', (radio = 2)) then radio := 2;
    if UIRadioButton('three', (radio = 3)) then radio := 3;

    UIEndWindow;

    UIEndFrame;

    DrawMouseCursor;
    WaitForVsync;
    Flip(BackbufferLayer);
end;

Some stuff doesn't work completely yet (such as the menus). As well, I'm missing two main widget types: textboxes and dropdown lists. Actually, I'm not expecting textbox widgets to be too difficult to implement. In fact, the input events system that I originally added to libDGL was originally added purely because I knew I was going to need such a thing to more easily implement such a textbox widget in the future!

However, I am relatively happy that things like automatic layout/positioning mostly works well, as well as automatic widget ID generation. Well, except that, as you can see above, there is an escape hatch for this in the form of UISetNextID that can be used when/if needed.

The idea with pretty much all of the widgets is that the widget function (e.g. UIButton, UICheckBox, etc) takes simple properties like a label string and maybe a couple other common style- or functionality-related properties as well as state variable(s) which are passed by pointer/reference to the widget. If the widget state changes, the variable(s) are updated immediately with the new state and the widget function will return a boolean to indicate that the state was changed. This boolean indicating that the "state was changed" is a bit context-dependant. For example, for a UIButton, it doesn't take in any explicit state variable itself (that is managed by the UI system in the form of widget IDs being tracked for what widget the mouse is currently hovering over, etc). But, the UIButton still returns true/false to indicate if it was clicked or not. For a UICheckBox which does require an explicit state variable to be passed to it, it returns true/false to indicate that that state variable was changed in response to a user action. This allows the application code to easily respond to the most common user interaction events for widgets with just simple if logic. Which is really, really nice actually! The previously linked video on immediate mode GUIs explains this idea really well and why it's nice for programs which do real-time rendering.

The key thing for me with this GUI code is that I do not want to create a totally generic UI framework. I only need to create something that will allow me to create some basic tools easily enough. Maybe eventually I'll want to add support for things like custom themes so that this could be used directly in an actual game project while matching the game's look and feel. Or, later on I might need more widget types. The list goes on, but I do not want to get lost in a never ending quest to get a "perfect" UI framework up and running, because well, it really would be a never ending quest and frankly, I don't need such a thing!

Right now I think I'm still going a bit too far with this by keeping support for things like tabbing between widgets. Will I actually use this feature in tools I create for myself? That remains to be seen (I suspect not, but you never know).

Some of the widgets leak implementation concerns a bit here and there which I'm not happy with. I've decided to just roll with it for now and hope that inspiration will kick in later and I'll be able to clean up these warts later on.

For example, the UIListBox widget requires two UI state variables to be provided to it. One of them, listSelection, is obviously needed (it holds the user's currently selection). The other one, listScrollY, holds the list's internal scrollbar scroll position and is kind of silly, but I didn't have any clever ideas for how to efficiently and automatically manage state of internal child widgets (the UIListBox widget internally uses a UIVertScrollBar widget).

Another example of this is in the menu code (which is mostly unimplemented as I write this). UIBeginMenu takes a second parameter which is the horizontal width of the menu to be created. This is kind of unfortunate, but I'm not exactly sure as of yet how to have this automatically figured out in a way that doesn't involve some kind of deferred rendering approach. I'm currently against the deferred rendering approach as I don't want to have to keep that kind of state around in memory somewhere... but it might be the best approach as it also solves another problem with my current approach which is that overlapping sub-menus would not render correctly. I might leave deferred rendering as a "v2.0" feature, heh.

Anyway, I'm hoping to have the UI stuff finalized in a workable state (even if it's not perfect) within the next week which will allow me to move onto creating tools for myself. Additionally, this UI code will need to be ported back to C so I can use it with libDGL eventually. I've been doing it with Turbo Pascal mainly for the reasons mentioned in my last post ... I find Turbo Pascal to be quite a bit faster for prototyping and experimentation.