Designing a GUI system, hints appreciated.

Started by
100 comments, last by Shinkage 13 years, 9 months ago
I have discovered IMGUI a couple of years ago and I first thought it was a really cool idea. But after several experiments and time spent trying to solve his problems, I came back to more traditional GUI systems. In my opinion, the only advantage of IMGUI is that it really simplify the implementation of very simple GUIs. In fact, some mouse and keyboard interaction, auto-layout systems and efficient drawing algorithms all relies on information which are simply not available in IMGUI systems. You can clearly try to make some intelligent caching and use data from the last frame, but that really complicate the GUI managment. After all of these tricks you will probably end up with something which isn't simpler than a RMGUI.

Recently I have discovered the data oriented paradigm and I have started thinking about a data oriented GUI. I have never implemented it, so this is just an abstract idea I want to share. It is in some ways the exact opposite of IMGUI since IMGUI UI are created using only code (there is no state or data retained by the system), while I have a very concrete representation of the UI as data and every interaction is implemented as a data transformation. A GUI is simply represented as a collection of widgets (you probably want some sort of tree but the actual implementation is quite free). It is just data and it does nothing, but this data structure contains every information needed to draw and implement the basic features of a general GUI. Each frame you then have to update and draw the GUI. GUI update is done in two steps. In the first step the window, mouse and keyboard events dispatched since the last frame are used to update the state of each widget and generates several additional GUI events (for example a button click). The transformation in the first step is part of the GUI system code. Then, in the second pass, the user should manage the events created in the first step and modify the GUI data structure accordingly. This updated GUI can then be rendered using a predefined function or by the user.
Advertisement
Quote:Original post by apatriarca
Recently I have discovered the data oriented paradigm and I have started thinking about a data oriented GUI. I have never implemented it, so this is just an abstract idea I want to share. It is in some ways the exact opposite of IMGUI since IMGUI UI are created using only code (there is no state or data retained by the system), while I have a very concrete representation of the UI as data and every interaction is implemented as a data transformation....
Your idea *is* pretty much an IMGUI system - why this point is so often overlooked is a mystery.

I don't think that anyone ever suggested that users of an IMGUI should write doButton() procedurally, for every GUI element they might need - and as you point out, this would only be suitable for very simplistic applications.

Instead, IMGUI makes it extremely easy to walk over a set of data (i.e. a YAML or XML file), and execute the relevant GUI functions as nodes are encountered. If you set this up correctly, you load a tree data structure from disk, which can be immediately passed into the IMGUI render/update function, and any transformations to the trees are immediately reflected in the GUI.

This data-driven approach is hard to realize in an RMGUI context, because every time the data is transformed, you have to copy the changed state to the relevant GUI elements, and add/remove elements to match. It isn't impossible, but the management overhead is substantial, and people rarely seem to implement data-driven GUIs for this very reason.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

Quote:Original post by Krohm
In "ID Generation and data lifetime" (link above) tmadden notes in a very well thought message that "The key [for dynamic IMGUIs] is that IDs don't identify individual widgets, they identify entire trees of static code.", he proposed a way to deal with this involving an anchor and a dynamic_block. I still have to fully digest his message, but it looks it's just the object-oriented version (admittedly correct and exception-safe) than my BeginGroup and EndGroup calls.

I'm tmadden, so if you have any questions, let me know. In hindsight, "ID generation" may have been a bad title for the post. I think too many people get hung up on trying to generate IDs for their widgets and then use those IDs as a key to a hash table to look up the associated data. Part of the beauty of IMGUI is that you can use if statements, for loops, and function calls in natural ways to represent the structure and dynamic behavior of your GUI, but in doing so, you lose the ability to identify a particular widget using some simple index or line number.

Instead, look at your code as a big tree of control flow. Normal (non-control flow) statements only have one child (the next statement), so they're just like links in a linked list. if statements are like branches in the tree. The left child is the first statement in the body of the if statement, and the right child is the statement after the if statement. When you hit a function call, just imagine that the code for that function is inlined at the spot of the call. Of course, with recursion, you'd have an infinite tree, but that's fine. Just imagine it.

Now imagine a parallel tree with the same structure but filled with data that accompanies those statements. This is exactly what you need for IMGUI. So what you need to build is a mechanism that walks that tree (or builds it if it doesn't exist yet) as code is being executed. You only need to know about two things: requests for data (these are the normal linked-list type nodes) and if statements. I use a macro to inform the library about if statements. So in my application, I can write...

alia_if (editing)    do_editing_ui(ctx);alia_else    do_viewing_ui(ctx);alia_end// more code


(alia is the name of the library)

...and the library ensures that any data requests inside "do_editing_ui" and "do_viewing_ui" go to separate parts of the tree, and that the "more code" part ends up with the same part of the tree each pass, no matter what the value of "editing" is. It's the same system I describe in the post. It's just a little more convenient now, thanks to macros.

Quote:This is more generally a very troublesome issue: no matter how smart the ID generation algorithm is, I just cannot see how it could deal with data shuffling, and this is a big problem.

What I describe above can't deal with data shuffling (though it can deal with loops in which the data doesn't shuffle around). That's when you actually need an ID. But as I said, you don't use an ID to identify an individual widget, you use it to identify the whole block of UI associated with that loop iteration.

There are two reasons for the anchor:

1) If an ID is absent from the UI for a frame, it could be for two reasons: a) it's hidden, or b) it was deleted from the list. You only want to delete the associated data if it's b. Otherwise, your UI would be reset every time you closed and reopened a tree control or flipped pages in a notebook. The anchor allows you to distinguish between the two cases.

2) You might want to reuse the same set of IDs in different parts of the UI. Having different anchors in the two places allows the same ID to map to different data in the two places.

Quote:Nonetheless, I feel like requiring the machinery he's suggesting coul be so complex one could just go for a standard RMGUI at this point!

It's not that much code once you understand it. My implementation is about 500 lines of C++, but this includes other features like support for switch statements.

Or you can just wait for me to release my implementation. :-) My boss has already agreed, so it's just a matter of me having time to get it ready for release, write docs, etc. I'm hoping to do it sometime this summer.

One other thing. Definitely go multi-pass. If you solve the data association problem, multi-pass becomes very easy and efficient (because you can cache everything), and implementing complex behaviors like layout is much easier in a multi-pass system.

There are some threads on Molly Rocket that have some stuff on multi-pass IMGUI: "IMGUI in graphics software" and "IMGUI and Render/Update separation".
I'm struggling to understand the utility of the IMGUI paradigm. Why is a GUI component having state and persisting a problem? All you're doing is storing that state and decision making elsewhere if you're simply constructing it (effectively) on every frame. In some instanced widget hierarchies you now have data binding, which is another way of modifying state in the model (but it's just a convenience, because you may still want to know when the underlying model was changed).

The best GUI's are those that are able to separate themselves from the underlying model. An IMGUI becomes more complex to implement as the sophistication of the GUI increases. That is why I would prefer the abstraction and flexibility of an instanced widget class hierarchy alongside a form of scene-graph for GUI components. None of this is particularly complicated or tedious to implement. An event handling system is also easy to code (it's just the observer pattern).


Quote:Original post by MoundS
I think too many people get hung up on trying to generate IDs for their widgets and then use those IDs as a key to a hash table to look up the associated data. Part of the beauty of IMGUI is that you can use if statements, for loops, and function calls in natural ways to represent the structure and dynamic behavior of your GUI, but in doing so, you lose the ability to identify a particular widget using some simple index or line number.

Instead, look at your code as a big tree of control flow...
It is incredible how much your rewording helped. I originally understood this with some differences - for example, I've truly modeled IFs in my head in a linear manner with some sort of predication going on, but it's the same for static code - what I didn't realize however was to get the true core of your statements.

You wrote this quite clearly:
Quote:The key here is that IDs don't identify individual widgets, they identify entire trees of static code.
Yet I wasn't able to understand this. I probably got this somehow but refused to accept the idea as it implies that widgets cannot really be IDentified with full confidence. This can be done only on certain cases when certain conditions are met, but the shuffling problem alone will break anyway.

But why I do want so much to ID? I don't plan to be able to poll state anyway, if not using the appropriate DoXXX calls. The problem is that if we cannot guarantee unique IDs application-side, which is definitely more information-rich, doing the same library-side is even more difficult.
Now, IDing means generating a unique control<->id correspondence, to be used as control<->id<->state - I am clearly considering only internal state here, such as appearance parameters, nothing the application cares about.
Unfortunately, if IDs cannot be uniquely identified then the library will have some trouble in associating the retained state across frames. But then, how can I remap the control to its associated state?

Say for example I want a button that shakes a bit on mouse-enter.
Now the button would be in static code, so its id would be unique. When in dynamic code, it would work anyway with lookup relative to its dynamic block. If I have the incremental ID in the block and the block id, then I can somehow walk to the correct internal state anyway, supposed data does not shuffle.
Quote:As for the choice of ID type, in the vast majority of cases, you can just ensure that your objects have fixed addresses and use pointers as IDs. Almost by definition, you only need one ID per dynamic object. If you have to deal with containers that can move your objects around (e.g, vectors), it's nice to be able to use other types, including tuples.
Now, having non-shuffling objects sounds feasible in many context, but as the machinery supporting this is still quite immature, I don't think I want to expose this.

I am surprised your implementation proven so compact but at this point I'm not yet sure I want to go that way at all. I recall doing a GL-based RMGUI system years ago and to be honest, I don't remember it giving me so much headaches.
Moreover, I spent some time looking at the interface mockups I have been given and I'm not sure I can make them fit.
As I said above, I'll probably want to use dual-frame IMGUI only for very simple GUIs at this point, for which having no IDs at all similarly to nvwidgets can work.
After all, immediate mode is about simplicity. IM in OpenGL implied no vertex sharing AFAIK, yet people used it anyway... I just have to trade off.
Quote:Original post by Burnhard
I'm struggling to understand the utility of the IMGUI paradigm. Why is a GUI component having state and persisting a problem?
It isn't. The problem is not that state persists but that generation and querying must be in two different places which must be somehow connected.
Quote:Original post by Burnhard
An IMGUI becomes more complex to implement as the sophistication of the GUI increases.
Agreed. After thinking at this for a while, I must add that the point at which a IMGUI becomes "complex" is possibly lower that I originally anticipated.
As I wrote above, I likely still want to build this and allow this to be used in scripts, but I moved the RMGUI system sooner in the future.

Wow, the thread now got 1024 hits!

Previously "Krohm"

Quote:Original post by Krohm
Quote:Original post by Burnhard
I'm struggling to understand the utility of the IMGUI paradigm. Why is a GUI component having state and persisting a problem?
It isn't. The problem is not that state persists but that generation and querying must be in two different places which must be somehow connected.
The real savings is in the amount of effort required to implement bi-directional propagation of state between the application (the 'model' in MVC parlance), and the GUI (or 'view').

Consider a fairly trivial example, of an application which maintains a list of items, and has a GUI to edit that list:

With an RMGUI, you iterate over the list of items at initialisation to build a list of items to display. When the user clicks the 'add' button, a message is sent to the application, which decides whether or not to add, an item is added to the list, and then a message must be sent back to the GUI to add the element to the list of displayed items. Want to edit an item? Message goes to application for validation, message goes back to the GUI to update the value. Want to delete an item? Same deal. Even when the application modifies an item internally, it has to send a message to the GUI to update the displayed item list.

If we go full-on Model-View-Controller, things just get even worse. Now the model doesn't know if/where it is being displayed, so it has to have an event system to notify the controller every time something changes. The view doesn't know what it is displaying, so it has to use events to notify the controller every time something changes. The controller needs buckets of glue code to connect the model and view together, and any change to model or view requires a matching change to the controller.

Apple solves this very elegantly through data binding, which effectively manages the controller layer for you through some library/language wizardry. However, it relies almost entirely on the highly dynamic nature of Objective-C, and isn't really reproducible in languages like C/C++.

An IMGUI allows you to implement a decent analogue of data binding in C/C++, and at the same time makes the whole concern fairly trivial. No need to propagate state in either direction, because the is only *one* set of state. No need to write buckets of glue code from controller to view, because the controller *is* the view.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

IMGUIs are such the shiny new hammer of interface design these days. Everything looks like a nail!

To contribute my own take on things, they both have their uses, but in my experience IMGUI is better suited to simple interfaces and RMGUI better suited to complex. The problem I have with IMGUI is that to really optimize it you end up having to cache what amounts to a complete duplicate of program state behind the scenes inside the GUI library, or else you'll end up redoing a lot of the presentation logic every frame. Since I work on mobile devices mostly of late, that kind of footprint really isn't acceptable, nor is the inefficiency of NOT caching all that data.

The solution I use is what amounts to a MVC framework, where the interface description is stored in external data and everything is hooked together through a signal/sink strategy using GUIDs to identify widgets (I have a custom GUID implementation that transparently switches between string-based [debug builds] and integer-based [release builds] that works quite well). It was a real pain to develop, but marvelously pleasant to use: generate the interface definition using an external tool, and it takes all of 2 lines of code to load it and display, then it can be hooked up to the program logic incrementally by simply binding to the signals it exposes. Definitely my favorite way of doing things at least.
Quote:Original post by Burnhard
I'm struggling to understand the utility of the IMGUI paradigm. Why is a GUI component having state and persisting a problem? All you're doing is storing that state and decision making elsewhere if you're simply constructing it (effectively) on every frame. In some instanced widget hierarchies you now have data binding, which is another way of modifying state in the model (but it's just a convenience, because you may still want to know when the underlying model was changed).

To understand IMGUI, you have to think about GUIs in a slightly different way. You seem to be thinking of them as a collection of objects that you want to manipulate as stuff happens, which is the way almost everyone has implemented GUIs since the beginning, but there's nothing inherent about a GUI that requires you to think of it like this. The reason for doing things this way is that when GUIs were first invented, computers were only fast enough to make minimal updates to the screen in response to user input, and representing the GUI as a collection of objects that the application would manipulate was a nice, easy way of quickly figuring out which parts of the screen would need updating.

But now it's 2010, and we're not operating under the same constraints, so we can use more natural ways of thinking. And a much more natural way is to think of your GUI is as a function of your application state. The function remains constant over time, but the state changes, and that's how you get changes in the on-screen UI. IMGUI is a direct implementation of this concept.

It can be difficult to understand if you're used to the old way, but once it clicks, it's a huge improvement.

Quote:The best GUI's are those that are able to separate themselves from the underlying model. An IMGUI becomes more complex to implement as the sophistication of the GUI increases. That is why I would prefer the abstraction and flexibility of an instanced widget class hierarchy alongside a form of scene-graph for GUI components. None of this is particularly complicated or tedious to implement. An event handling system is also easy to code (it's just the observer pattern).

Well, you should distinguish between the library and the application. An IMGUI library is more complex to implement than an RMGUI library. IMGUI applications are much simpler to implement because most of the complexity has been moved into the library. It's always a personal decision as to how much time you want to invest in factoring out complexity like this, so I can understand if you don't consider it worthwhile to implement your own IMGUI library. I'm sure one day there will be readily available solutions for all the popular languages, so that people don't have to make the investment for themselves.

Note, however, that it's this kind of refactoring and abstraction that allows you to implement more complex applications, and to make them more flexible, so in fact IMGUI scales much better than RMGUI.

Also, in my experience, IMGUI allows a much better separation between model and view. With the observer pattern, the data you're observing has to be designed with that pattern in mind. With IMGUI, there's no such requirement.

Quote:Original post by Shinkage
IMGUIs are such the shiny new hammer of interface design these days. Everything looks like a nail!

To contribute my own take on things, they both have their uses, but in my experience IMGUI is better suited to simple interfaces and RMGUI better suited to complex. The problem I have with IMGUI is that to really optimize it you end up having to cache what amounts to a complete duplicate of program state behind the scenes inside the GUI library, or else you'll end up redoing a lot of the presentation logic every frame. Since I work on mobile devices mostly of late, that kind of footprint really isn't acceptable, nor is the inefficiency of NOT caching all that data.

Can you explain why you think RMGUI is better for complex GUIs? How much actual experience do you have with IMGUI? I'm just curious.

As for the caching, the data that an IMGUI caches is the same data that an RMGUI would store in the widget objects, so why is that less efficient?
Quote:Original post by MoundS
Can you explain why you think RMGUI is better for complex GUIs? How much actual experience do you have with IMGUI? I'm just curious.

As for the caching, the data that an IMGUI caches is the same data that an RMGUI would store in the widget objects, so why is that less efficient?


So if the IMGUI is caching the same data that a RMGUI would store in its widget objects, what you really have is just an IM facade over an RM implementation. So if it really amounts just to your preferred syntactic sugar one way of the other, here is why I don't favor IMGUIs for sufficiently complex interfaces:
if(button1()) {}if(button2()) {}...if(button132()) {}...

Fill in the ellipses there with several score various widgets and logic, and then call this all every frame. Here's an observation of mine from past UI design: in a complex UI, most of the time nothing happens. In other words, for any given widget in any given frame, odds are that its state will be the same as it was last frame. In simpler UIs this isn't the case so much. Think the difference between Eve Online or Civilization (complex) and Quake (simple) to see what I'm getting at here.

So anyway, that's why I favor IMGUIs less for sufficiently complex interfaces: the kind of neurotic polling and updating the IM paradigm does every frame just becomes wasteful. This can get especially bad if your interface logic is partly implemented in some kind of scripting language (as the case with my current project), where calling in to the script isn't an especially cheap operation.

Well, that's my thinking and experience behind that claim I made at least :)
Quote:Original post by Shinkage
So anyway, that's why I favor IMGUIs less for sufficiently complex interfaces: the kind of neurotic polling and updating the IM paradigm does every frame just becomes wasteful.
Why do you feel it is necessary to continually poll/update your IMGUI? Treat it exactly as you would any RMGUI, and only update when input is available, or when you need to redraw. Better yet, make your IMGUI multi-pass, so that you only invoke the necessary portion on input/drawing.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

This topic is closed to new replies.

Advertisement