In the first part of the GUI tutorial (
link), we have seen the positioning and dimension system.
You can also look at other chapters:
Today, before rendering, we will spend some time and familiarize ourselves with basic element types used in this tutorial. Of course, feel free and design anything you like. The controls mentioned in this part are some sort of a standard, that every GUI should have. Those are
- Panel - usually not rendered, used only to group elements with similar functionality. You can easily move all of its content or hide it.
- Button - what else to say. Button is just a plain old button
- Checkbox - in basic principles similar to button, but has more states. We all probably know it.
- Image - can be used for icons, image visualization etc.
- TextCaption - for text rendering
Control logic
The control logic is maintained from one class. This class is taking care of changes of states and contains reference to the actual control mechanism - mouse or touch controller. So far, only single touch is solved. If we want to have a multi-touch GUI control, it will be more complicated. We would need to solve problems and actions, if one finger is down and the other is "moving" across screen. What happens if a moving finger crosses an element, that is already active? What if we release the first finger and keep only the second, that arrived on our element during movement? Those questions can be solved by observing existing GUI systems how they behave, but what if there are more systems and every one of them behaves differently? Which one is more correct? Due to all those questions, I have disabled multi-touch support. For main menu and other similar screens, it is usually OK. Problems can be caused by the main game. If we are creating for example a racing game, we need to have multi-touch support. One finger controls pedals and the other steering, and third one maybe shifting. For these types of situations, we need multi-touch support. But that will not be described here, since I have not used it so far. I have it in mind and I believe the described system can easily be upgraded to support this.
For each element we need to test a position of our control point (mouse, finger) against the element. We use positions calculated in the previous article. Since every element is basicly a 2D AABB (axis aligned bounding box), we can use a simple interval testing in axes X and Y. Note, that we test only visible elements. If a point is inside an invisible element, we usually discard it and continue.
We need to solve one more thing. If elements are inside each other, which one will receive the action? I have used a simple depth testing. A screen, as a parent to all other elements, has depth 0. Every child within the screen has depth = parentDepth + offset. And so on, recursively for children of children. A found element with the biggest depth and point inside is called "with focus". We will use this naming convention in later parts.
I have three basic states for a user controller
- CONTROL_NONE - no control button is pressed
- CONTROL_OVER - controller is over, but no button is pressed
- CONTROL_CLICK - controller is over and a button is pressed
This is 1:1 applicable to a mouse controller. For fingers and a touch control in general, CONTROL_OVER state has no real meaning. To keep things simple and portable, we preserve this state and handle it in a code logic with some condition parts. For this I have used a prepsocessor (
#ifdef), but it can also be decided in a runtime with a simple
if branch.
Once we identify an element with current focus, we need to do several things. First of all, compare last and actual focused elements. I will explain this idea on a commented code.
if (last != NULL) //there was some focused element as last
{
//update state of last focused element as currently no control state
UpdateElementState(CONTROL_NONE, last);
}
if (control->IsPressed())
{
//test current state of control (mouse / finger)
//if control is down, do not trigger state change for mouse over
return;
}
if (act != NULL)
{
//set state of current element as control (mouse / finger) over
//if control is mouse - this will change state to HOVERED, with finger
//it will go directly to same state as mouse down
UpdateElementState(CONTROL_OVER, act);
}
If last and actual focused elements are the same, we need to do a different chain of responses.
if (act == NULL)
{
//no selected element - no clicking on it => do nothing
return;
}
if (control->IsPressed())
{
//control (mouse / finger) is down - send state to element
UpdateElementState(CONTROL_CLICK, act);
}
if (control->IsReleased())
{
//control (mouse / finger) is released - send state to element
UpdateElementState(CONTROL_OVER, act);
}
In the above code, tests on
NULL are important, since if no element is focused at the moment,
NULL is used for this state. Also, control states are sent in every update, so we need to figure how to change them into element states and how to correctly call triggers.
An element changes and trigger actions are now special for a different types of elements. I will sumarize them in a following section. To fire up triggers, I have used delegetes from FastDelegate library / header (
Member Function Pointers and the Fastest Possible C++ Delegates). This library is very easy to use and is perfectly portable (iOS, Android, Win...). In C++11 there are some built-in solutions, but I woud rather stick with this library.
For each element that need some triggers, I add them via
Set functions. If the associated action is triggered the delegate is called. Instead of this, you can use function pointers. Problem with them is usually associated with classes and member functions. With delagates, you will have easy to maintain code and you can associate delagets with classic functions or meber functions. In both cases, the code remains the same. Only difference will be in a delegate creation (but for this, see article about this topic on codeproject - link above).
In C#, you have delegates in language core support, so there is no problem at all. In Java, there is probably also some solution, but I am not Java positive, so I dont know this :-) For other languages, there will also be some similar functionality.
Elements
First, there is a good reason to create an abstract element that every other will extend. In that abstract element, you will have position, dimensions, color and some other useful things. The specialized functionality will be coded in separate classes that extend this abstract class.
1. Panel & Image
Both of them have no real functionality. A panel exists simply for grouping elements together and an image for showing images. That's all. Basically, both of them are very similar. You can choose its background color or set some texture on it. The reason why I have created two different elements is for better readibility of code and Image has some more background functionality, like helper methods for render targets visualization (used in debugging shadow maps, deferred rendering etc.).
1.1 Control logic
Well... here it is really simple. I am using no states for these two elements. Of course, feel free to add some of them.
2. Button
One of the two more interesting elements I am going to investigate in detail. A button is reccomended as a first for what you should code, when you are creating a GUI. You can try various scenarios on it - show texture, change texture, control interaction, rendering etc. Other elements are basically just a modified button :-)
Our button has three states
- non active - classic default state
- hovered - this is correct only for mouse control and indicates that a mouse pointer is over the element, but no mouse button is pressed. This state is not used for a touch control
- active - button has been clicked or mouse / finger has been pressed on top of it
You could add more states, but those three are all you need for basic effects and a nice looking button.
You should have at least two different textures for each button. One that indicates the default state and the one used for an action state. There is often no need to separate active and hovered state, they can look the same. On a touch controller there is even no hovered state, so there is no difference at all.
Closely related to the state changes are triggers. Those are actions that will occur when a button state changes from one to another or if it is in some state. You can think of many possible actions (if you don't know, the best way is to look for example into C# properties for UI button). I have used only a limited set of triggers. My basic used ones are
- onDown - mouse or finger has been pressed on the button
- onClick - click is generated after releasing pressed control (with some additional prerequisites)
- onHover - valid only for mouse control. Mouse is on the button, but not pressed
- onUp - mouse or finger has been released on the button (it can be seen similar to onClick without additional prerequisites)
- whileDown - called while button mouse or finger is pressed on the button
- whileHover - called while button mouse is on the button, but not pressed
I have almost never seen "while" triggers. In my oppinion, they are good for repeating actions, like a throttle pedal in a touch-based racing game. You are holding it most of the time.
Sometimes, you need functionality similar to a checkbox with a button. Typical case is a "play / pause" button in a media player. Once you hit the button, an action is trigerred and also the icon is changed. You can either use a real checkbox or alter the button a little bit (that is what I am using). In a trigger action code, you just simply change the icon set used for the button. See a sample code. In this I am using a button as a checkbox to enable / disable sound.
void OnClickAction(GUISystem::GUIElement * el)
{
//emulate check box behaviour with button
if (this->sound_on)
{
//sound is currently on - we are turning it off
//change icon set
GUISystem::GUIButtonTextures t;
t.textureName = "soundoff"; //default texture
t.textureNameClicked = "soundon"; //on click
t.textureNameHover = "soundon"; //on mouse over
el->GetButton()->SetTextures(t);
}
else
{
//sound is currently off - we are turning it on
//change icon set
GUISystem::GUIButtonTextures t;
t.textureName = "soundon"; //default texture
t.textureNameClicked = "soundoff"; //on click
t.textureNameHover = "soundoff"; //on mouse over
el->GetButton()->SetTextures(t);
}
//do some other actions needed to enable / disable sound
}
2.1. Control logic
Control logic of a button seems relatively simple from already mentioned states. There are only three basic ones. Hovewer, main code is a bit more complex. I have divided implementation into two parts. First is a "message" (basically it is not a message, it's just some function call, but it can be seen as a message) sending on a state change to the button from a controller class and a second part handles a state change and trigger calls based on a received "message". This part is coded directly inside a button class implementation.
First part, inside a control class, that is sending "messages".
if (ctrl == CONTROL_OVER) //element has focus from mouse
{
#ifdef __TOUCH_CONTROL__
//touch control has no CONTROL_OVER state !
//CONTROL_OVER => element has been touched => CONTROL_CLICK
//convert it to CONTROL_CLICK
ctrl = CONTROL_CLICK;
#else
//should not occur for touch control
if (btn->GetState() == BTN_STATE_CLICKED) //last was clicked
{
btn->SetState(BTN_STATE_NON_ACTIVE); //trigger actions for onRelease
btn->SetState(BTN_STATE_OVER); //hover it - mouse stays on top of element after click
//that is important, otherwise it will look odd
}
else
{
btn->SetState(BTN_STATE_OVER); //hover element
}
#endif
}
if (ctrl == CONTROL_CLICK) //element has focus from mouse and we have touched mouse button
{
btn->SetState(BTN_STATE_CLICKED); //trigger actions for onClick
}
if (ctrl == CONTROL_NONE) //element has no mouse focus
{
#ifndef __TOUCH_CONTROL__
btn->SetState(BTN_STATE_OVER); //deactivate (on over)
#endif
if (control->IsPressed())
{
btn->SetState(BTN_STATE_DUMMY); //deactivate - use dummy state to prevent some actions
//associtaed with releasing control (most of the time used in touch control)
}
btn->SetState(BTN_STATE_NON_ACTIVE); //deactivate
}
Second part is coded inside a button and handles received "messages". Touch control difference is also covered (a button should never receive a hover state). Of course, sometimes you want to preserve hover state to port your application and keep the same functionality. In that case, hover trigger is often called together with
onDown.
if (this->actState == newState)
{
//call repeat triggers
if ((this->hasBeenDown) && (this->actState == BTN_STATE_CLICKED))
{
//call whileDown trigger
}
if (this->actState == BTN_STATE_OVER)
{
//call while hover trigger
}
return;
}
//handle state change
if (newState == BTN_STATE_DUMMY)
{
//dummy state to "erase" safely states without trigger
//delegates associated with action
//dummy = NON_ACTIVE state
this->actState = BTN_STATE_NON_ACTIVE;
return;
}
//was not active => now mouse over
if ((this->actState == BTN_STATE_NON_ACTIVE) && (newState == BTN_STATE_OVER))
{
//trigger onHover
}
//was clicked => now non active
if ((this->actState == BTN_STATE_CLICKED) && (newState == BTN_STATE_NON_ACTIVE))
{
if (this->hasBeenDown)
{
//trigger onClick
}
else
{
//trigger onUp
}
}
#ifdef __TOUCH_CONTROL__
//no hover state on touch control => go directly from NON_ACTIVE to CLICKED
if ((this->actState == BTN_STATE_NON_ACTIVE) && (newState == BTN_STATE_CLICKED))
#else
//go from mouse OVER state to CLICKED
if ((this->actState == BTN_STATE_OVER) && (newState == BTN_STATE_CLICKED))
#endif
{
this->hasBeenDown = true;
//trigger onDown
}
else
{
this->hasBeenDown = false;
}
this->actState = newState;
Code I have shown is almost everything that handles a button control.
3. Checkbox
Second complex element is a checkbox. Its functionality is similar to a button, but it has more states. I will not describe state changes and handling of those as detailed as I have done for a button. It's very similar, you can learn from button code and extend it. Plus it would take a little bit more space.
Our checkbox has six states
- non active - classic default state
- hovered - this control is correct only for a mouse control and indicates that mouse pointer is over element, but no mouse button is pressed. This state is not used in touch controls
- clicked - state right after it has been clicked => in next "frame" state will be checked
- checked - checkbox is checked. We go to this state after clicked state
- checked + hovered - for checked state we need to have different hover state. It makes sense, since icon is usually also different
- checked + clicked - state right after it has been clicked in checked state => next "frame" state will be non active
You will need two different sets of textures, one for unchecked and one for checked states. As for triggers, you can use the same as for a button, but with two additional ones.
- onCheck - state of the checkbox has been changed to checked
- onUncheck- state of the checkbox has been changed to unchecked
"While" triggers can be also used together with check state, like
whileChecked. Hovewer, I don't see a real use for this at the moment.
3.1. Control logic
Control logic is, in its basic sense, similar to a button. You only need to handle more states. If you are lazy, you can even discard the checkbox all together and simulate its behavior with a simple button. You will put a code into an
onClick trigger action. In there you will have to change the texture of the button. The is one set of textures for non-checked states and the second set for checked states and you will just swap them if one or the other state occurs. This will only affect visual appereance of the element, you will have no special triggers, like
onCheck. You can emulate those with button triggers and some temporary variables.
4. Text caption
Text caption is a very simple element. It has no specific texture, but contains words and letters. It's used only for small captions, so it can be added on top of a button to create a caption. If you need texts that are longer, you have to add some special functionality. This basic element is only for very simple texts (one line, no wrap if text is too long etc).
More advance text elements should support multi-lines, auto wrap of a text if it is too long, padding or just anything else you can think of.
4.1. Control logic
Text caption has no control logic. Its only purpose is to show you some text :-)
Discussion
In the second part of our "tutorial" I have covered basic elements that you will need most of the time. Without those, no GUI can be complete. I have shown more details for a button, since a checkbox is very similar and can be emulated with a simple button and some temporary variables (and some magic, of course).
If you think something can be done better or is not accurate, feel free and post a comment. In an attachment, you can download source code (C++) for a descibed functionality. Code is not usable as it is, because of the dependencies on the rest of my engine.
In future parts, we will investigate rendering and some tips & tricks.
Article Update Log
22 May 2014: Added links to other parts of tutorial
10 May 2014: Added missing code, added description of checkbox emulation with button
9 May 2014: Initial release
I've already reminded Martin he forgot to upload the code attachment