A short introduction to glGUI

Introduction

glGUI is a system to set up a graphical user interface (GUI). It has be designed to be used in conjunction with openGL and GLUT, but is not restricted to this environment. Examples for use would be games or virtual or mixed reality applications, where the traditional GUI elements of the host operating system are not appropriate to use. Emphasized like this are open issues, i.e. questions and features scheduled for implementation.

General Structure

Although glGUI has be designed and tested with openGL, it does not depend on openGL. All calls to the graphical window are centralized in the methods of two classes, the shader and the renderer. glGUI itself has a single entry, 'render', which recursively goes thru the widgets and renders them using the external shader and renderer. In a similar way, the two entries for keyboard and mouse events are going thru the widgets tree, call the action callback if applicable, and return.

In a certain sense, glGUI is a scenegraph system with user event routing and external rendering, with a restricted class of graphical objects, called widgets.

The Structure of a sample Application

Main Object

At first, a single main object of class CUI_UI has to be created:
CUI_UI *ui;
int main(...) {
        ui = new CUI_UI();
This object is the root of the whole UI data structure; it keeps track of the UI structure, calls the external renderer and is called for mouse and keyboard events.
Additionally, it contains a simple string-adressed repository to store a number of attributes and properties to be used with the widget objects later. To use glGUI, a reference to this object needs to be present, so the UI object is stored in a global variable.

Register the Renderer

Next, the external renderer is created and given to the main object. So, the above code snippet reads now:
CUI_UI *ui;
CUI_OpenGLRenderer *renderer;
int main(...) {
    ui = new CUI_UI();
    renderer = new CUI_OpenGLRenderer();
    ui->SetRenderer( renderer );
A pointer to the renderer object is made global, because the sample application uses a function LoadTexture of the renderer to load a texture. As text characters are simply special textures, a function like this is nearly always necessary.
The GetRenderer function returns a reference to the renderer object, so only a pointer to the CUI_UI object must be available.

The Shader and other helper objects

Whereas there is only one renderer object, the SimpleShader class is used to create many small objects, each for any used color. There are more small objects like this, that are generally stored in the registry for later use.

Thus, the default shades and commonly used colors are defined next:

        v2_f defborder[] = { {0,0}, {1,0}, {1,1}, {0,1} };
        ui->AddShader( "default", new CUI_SimpleShader() );
        ui->AddCoordSys( "default", new CUI_CoordSys() );
        ui->AddBorder( "default", new CUI_PolyEdgeBorder( defborder, 4);
        ui->AddShader( "white", new CUI_SimpleShader( 1.0, 1.0, 1.0, 1.0 ) );
         ...
Here a number of helper objects are created and memorized via the glGUI registry under a string name.

The frame widget

Unless read in from a file, the structure of the user interface can now be generated.

A main panel or frame is the container for the other widgets like buttons:

CUI_Frame *mainF = new CUI_Frame();
        mainF->SetShader( ui->GetShader("grey") );
        ui->AddRootFrame( "intro", mainF );
        ui->SetActiveRootFrame( "intro" );
        ui->SetCallbackFunc( "intro", introHandler );
It is added as a root frame and then declared the current active frame, which is the frame to be rendered from now on. An arbitrary number of such root frames containing UI widgets may be defined and switched as required.

Most often, the callback for mouse and keyboard actions is already registered here with the call to SetCallbackFunc.

The inner widgets

Next, a tree of widgets will be defined, in this case a title box:
CUI_Widget *title = new CUI_Widget();
        title->Move( 0.1, 0.85, 0.35, 0.95 );  // x left, y bottom, x right, y top
        title->SetShader( ui->GetShader( "black" ) );
        title->SetText( "glGUI   " );
        mainF->AddChild( title );
        ui->AddFrame( title );
A new widget is created, sized and positioned, colored, and given a text. (The - default - font in the example is white, so black is a useful background).
Thhis widget must be registered twice: with the main frame, so that it is an element inside the main frame; and with the CUI_UI object. The reason for this double registration is not yet clear.

As far as more widgets are needed, this last step has to be repeated, e.g. for a mouse button:



Rendering the User interface

The principal function of glGUI can be seen as a redering filter, where the concrete rendering is performed by the initially created renderer, e.g. the CUI_OpenGLRenderer. Unlike many other user interfaces, glGUI does not have its own main loop, but must be called during the rendering process at appropriate times.

In general, the user interface should not be hidden, obscured or garbled by other graphical objects, thus rendered last.

Redering is performed by calling the render method of the main CUI_UI object. In a typcial GLUT application this will be in the update callback function registered via glutDisplayFunc, which corresponds to the refesh callback in Windows.

     ui->UpdateStatus();
     // save and set display status,
     // e.g. PROJECTION and MODELVIEW matrix
     ui->Render( dTime );
     // restore matrices
The UpdateStatus method updates the internal flags depending on previous mouse or keyboard actions, so that e.g. color changes of buttons by hoovering and clicking are visible. Cases where there is no status update before rendering are not yet known.

Depending on the implemetation of the rendering function, the graphic context has to be set. In case of openGL, the rendering interface should set (and restore) all flags and attributes that are always necessary, leaving the other to the caller.

The render call should be last in the refresh callback of the host system, so that the widgets are not damaged by other graphical objects. Widgets may be made transparent, so that the underlying scene is not completely obscured.

The render method of CUI_UI recursively calls the external rendering for each widget in the proper order, delivering the corresponding shaders as registered with each widget.

Keyboard and Mouse Events

User input events are processed independently of the rendering. The method UpdateStatus mentioned above links the input events to the graphical feedback.

Two methods are available for keyboard and mouse actions:

        (bool) ui->UpdateCursor( x, y, state );
        (bool) ui->ProcessKey( key );
UpdateCursor traverses the tree of widgets and checks if a (clickable) frame is at the cursor position. If it is, it will return true, and it will return false otherwise. A callback function will be called if an active widget is hit. Currently, the callback function can be registerd with a main frame only; future versions might allow callback functions for any clickable widget.

UpdateCursor saves the cursor position and displays any mouse cursor next time the rendering is called.

ProcessKey is used for keyboard input. glGUI tries the key input handler of the active widget which may process the key itself or pass it back, where some default action will be done. The logic for key processing is not yet clear.

The callback function currently receives the number of the widget affected as a long (unsigned) integer.
This interface is clumsy. The user has to track the assigned numbers and build himself another registry to save relevant data. The user may have objects of his own corresponding to the widgets of glGUI and like to call pure virtual functions depending of the widget affected.
Solutions may be:

  1. Provide in the action callback the id as before and an extra user data parameter, stored in the main frame. The specific widget that was target of the user input is still only identified by the id number.
  2. Provide the id as before and an extra user data parameter, but from the affected widget instead of the main frame.
  3. The id is not restricted to a long integer, but is declared a union of a long integer and pointer. Backwards-compatible solution requiring only some source code cleanup. The user is free to store an id number, a pointer to the widget or to one of its own objects, but only one of these.
  4. Instead of a long integer id, a pointer to the affected widget is given. A per-widget user data field is needed then, accessed by a method from the given pointer to the widget.
  5. The callback provides: a pointer to the specific widget activated, a pointer to the main frame, and some user data set before.
The first one would be easiest to implement, but neither sufficient nor compatible.
The second one would not clean up the structure.
The third one would be a clean and minimalistic soulution, still easy to implement, and fully backwards compatible.
The fourth one would be clean and more flexible.
The fifth one would be most flexible and require the greatest effort in implemation.