Last time (Part 1) I finished by drawing a couple of rectangles on the screen. But I missed the bug, so let's fix it. Let's have a look at the code below, can you spot it?
void draw(system *s)
{
// ...
for (usize i = 0; i < s->shapes.size(); i++)
{
element *e = s->shapes.data() + i;
auto model = e->transform_to_root;
auto view = math::matrix::identity();
auto rect = rectangle2::from_center_size(V2(0, 0), 100, 100);
draw_rectangle(model, view, projection, rect, e->color); // @todo: implement this with OpenGL
}
}
I draw all the elements in a forward direction. Here is what it would look like:
Bottom elements on top, and top elements on bottom. This is because I am not using Z-buffer to draw the UI. This is how I would expect things to be drawn:
So let's draw them in the reverse order. Good thing that it's really easy to do:
void draw(system *s)
{
// ...
for (usize i = s->shapes.size() - 1; i < s->shapes.size(); i--)
{
element *e = s->shapes.data() + i;
auto model = e->transform_to_root;
auto view = math::matrix::identity();
auto rect = rectangle2::from_center_size(V2(0, 0), 100, 100);
draw_rectangle(model, view, projection, rect, e->color); // @todo: implement this with OpenGL
}
}
Here I take advantage of the fact that when i
goes below 0, it wraps around and becomes a huge number. Note that this is not UB (undefined behaviour), wrapping is defined in the standard for unsigned integers.
Normally, events such as hovering and clicking are handled through the event system. Each element sends an event to its parent or child, propagating events through the tree of elements. When an event is received, the element decides whether to send it on or stop and do something.
I find this a bit cumbersome. I want something simpler for my UI library.
I recently watched the talk where Casey Muratori explains the concept of Immediate UI (https://youtu.be/Z1qyvQsjK5Y), here's how he handles hovering and clicking.
Each UI element has the two states it can be in: "hot state", which is when the mouse cursor is over the element, and the user is about to interact with the element, and "active state", which is when the user is actually interacting with the element.
For example, when user hovers over the the button, the button may start blinking inviting the user to click on it (button is hot), and if the button is clicked, the appearance also changes to the clicked button (it becomes active).
A button can be made of many things: graphics, text, animations, alpha masks, etc. How do we know what element is responsible for this behaviour? What's the solution for that?
I think of a button as just a container with the specific behaviour: being hoverable and clickable. So any UI group could be a button, as long as it has those components.
Let's make the struct for those components:
struct hover_behaviour
{
element *owner;
rectangle2 hover_area;
};
struct click_behaviour
{
element *owner;
rectangle2 click_area;
};
struct system
{
// ...
array<element> groups;
array<element> shapes;
array<hover_behaviour> hoverables;
array<click_behaviour> clickables;
element *hot;
element *active;
// ...
};
Here I have two new structures to represent the ability of the element to be hovered over and clicked. Let's add them to the element
structure.
struct element
{
// ...
hover_behaviour *hoverable;
click_behaviour *clickable;
};
So, if these pointers are equal to NULL
, I will consider the element not hoverable or clickable. If they will point to the "behaviour" structure, I will consider them to have that type of behaviour.
There are really two types of response that an element can make after an event occurs: internal and external. Think about it, when the button is clicked, the code that makes an animation should not be interfere with the code that makes a custom user defined response, right? So I will define two callbacks for each of the "events", like this:
typedef void ui_callback(system *, element *);
void ui_callback_stub(system *, element *) {}
struct hover_behaviour
{
// ...
ui_callback *on_enter;
ui_callback *on_leave;
ui_callback *on_enter_internal;
ui_callback *on_leave_internal;
};
struct click_behaviour
{
// ...
ui_callback *on_press;
ui_callback *on_release;
ui_callback *on_press_internal;
ui_callback *on_release_internal;
It's you!
But seriously, it's really easy to do. We do a loop the entire hoverables
array, find the mouse position in the local coordinate system of that element, and see if it's inside the hover_area
rectangle.
void update(system *s, input_devices *input)
{
for (usize i = 0; i < s->hoverables.size(); i++)
{
hover_behaviour *behaviour = s->hoverables.data() + i;
element *owner = behaviour->owner;
auto inverse_transform = math::inverse(owner->transform_to_root);
auto mouse_position_local = inverse_transform * input->mouse.position;
if (is_inside(behaviour->hover_area, mouse_position_local))
{
s->hot = owner;
break;
}
}
}
But, wait a minute, what if these hover components are stored out of order? Let's look at the possible situation here:
Let's imagine that our mouse is placed in the intersection of two bounding boxes, how would we determine that the green triangle is on top of the blue circle? I propose this solution: before deciding which element is hot, let's enumerate all elements with the order index.
It would look like this:
But now we can determine which element is higher up in the UI hierarchy. To determine this order index we could run DFS (depth first search) with the l-value reference as the parameter, which will increment at each step.
struct element
{
uint32 order_index;
// ...
void update_order_index(element *e, uint32& order_index)
{
e->order_index = order_index++;
for (usize child_index = 0; child_index < e->children.size; child_index++)
{
element *child = e->children.data[child_index];
update_order_index(child, order_index);
}
}
void update(system *s, input_devices *input)
{
uint32 order_index = 0;
update_order_index(&s->root, order_index);
// Now we can use e->order_index as the sorting criteria
element *hovered = NULL;
for (usize i = 0; i < s->hoverables.size(); i++)
{
hover_behaviour *behaviour = s->hoverables.data() + i;
element *owner = behaviour->owner;
auto inverse_transform = math::inverse(owner->transform_to_root);
auto mouse_position_local = inverse_transform * input->mouse.position;
if (is_inside(behaviour->hover_area, mouse_position_local))
{
if ((hovered == NULL) ||
(owner->order_index < hovered->order_index))
{
hovered = owner;
}
}
}
if (hovered)
{
// I found element under mouse! That's good.
if (s->hot != NULL)
{
// There's something hot already, check if it's what I found.
if (s->hot == hovered)
{
// The element I found under the mouse is exactly what I have hot. I will do nothing then.
}
else
{
// This is new element! Remove hot from old one and make hot a new one!
make_cold(s, s->hot);
make_hot(s, hovered);
}
}
else
{
// There's nothing hot yet, let's make our element hot.
make_hot(s, hovered);
}
}
else
{
// I didn't find anything under a mouse, so if there's anything hot, I should make it cold.
if (s->hot) make_cold(s, s->hot);
}
}
I hope this code is simple enough that anyone can read it, although it looks a bit scary and big because of the way I place curly braces.
The make_hot
and make_cold
functions just set s->hot
to a pointer of an element or NULL
. But what if we need to respond to an event?
That's easy, we already have two functions that should do that:
hover_behaviour *make_hoverable(system *s, element *e)
{
hover_behaviour *result = NULL;
if (e->hoverable == NULL)
{
result = s->hoverables.push();
result->owner = e;
result->hover_area = math::rectangle2::from_center_size(V2(0), 100, 100);
// Fill in all callbacks with stubs
result->on_enter = callback_stub;
result->on_leave = callback_stub;
result->on_enter_internal = callback_stub;
result->on_leave_internal = callback_stub;
e->hoverable = result;
}
// Return pointer so user can fill in callbacks as he wishes
return result;
}
void make_cold(system *s, element *e)
{
if (s->hot == e)
{
s->hot->hoverable->on_leave_internal(s, s->hot);
s->hot->hoverable->on_leave(s, s->hot);
s->hot = NULL;
}
}
void make_hot(system *s, element *e)
{
s->hot = e;
s->hot->hoverable->on_enter(s, s->hot);
s->hot->hoverable->on_enter_internal(s, s->hot);
}
So now we can make rectangles that change color when the mouse is over them. Let's do that:
auto group_1 = ui::make_group(&ui, &ui.root);
auto shape_1 = ui::make_shape(&ui, group_1);
shape_1->position = V2(500, 600);
shape_1->rotation = 20.f;
shape_1->color = V4(0.9, 0.4, 0.2, 1.0);
auto hoverable_1 = ui::make_hoverable(&ui, shape_1);
hoverable_1->on_enter_internal = [](ui::system *s, ui::element *e)
{
e->color = V4(1, 0, 0, 1);
};
hoverable_1->on_leave_internal = [](ui::system *s, ui::element *e)
{
e->color = V4(0.9, 0.4, 0.2, 1.0);
};
And the result is here:
Now, when you see pipepline completely, you can see that adding a click is very easy to do. The first thing that is different, is that we can click only on hot elements, so we need hover_behaviour to make a button. The second difference, that when you release the mouse button, but the active element is no longer hot, we do not register a click, but internally, we should release the button and start internal graphics animation for the button release.
So, the full implementation of our update function will look like this:
void update(system *s, input_devices *input)
{
uint32 order_index = 0;
update_order_index(&s->root, order_index);
element *hovered = NULL;
for (usize i = 0; i < s->hoverables.size; i++)
{
hover_behaviour *hoverable = s->hoverables.data() + i;
auto inverse_transform = inverse(hoverable->owner->transform_to_root);
auto mouse_position_local = inverse_transform * input->mouse.position;
if (math::is_inside(hoverable->hover_area, mouse_position_local.xy))
{
if ((hovered == NULL) ||
(hoverable->owner->order_index < hovered->order_index))
{
hovered = hoverable->owner;
}
}
}
if (hovered)
{
if (s->active == NULL)
{
// I found element under mouse, and no element is active! That's good, I can set it as hot.
if (s->hot != NULL)
{
// There's something hot already, check if it's what I found.
if (s->hot == hovered)
{
// The element I found under the mouse is exactly what I have hot. I will do nothing then.
}
else
{
// This is new element! Remove hot from old one and make hot a new one!
make_cold(s, s->hot);
make_hot(s, hovered);
}
}
else
{
// There's nothing hot yet, let's make our element hot.
make_hot(s, hovered);
}
}
else
{
// @todo: When this happens?
}
}
else
{
if (s->hot)
{
// I didn't find anything under the mouse, but I have a hot element? Make it cold again.
make_cold(s, s->hot);
}
}
if (get_press_count(inp->mouse[mouse_device::LMB]))
{
s->active = s->hot;
if (s->active)
{
if (s->active->clickable)
{
s->active->clickable->on_press_internal(s, s->active);
s->active->clickable->on_press(s, s->active);
}
}
}
if (get_release_count(inp->mouse[mouse_device::LMB]))
{
if (s->active)
{
if (s->active->clickable)
{
if (s->active == s->hot)
{
s->active->clickable->on_release(s, s->active);
}
s->active->clickable->on_release_internal(s, s->active);
}
s->active = NULL;
}
}
// @note: This should be applied each frame after update phase, right?
update_transforms(s);
}
If you want to know what are the get_press_count
and get_release_count
functions, I can refer you to this little note of mine: Data oriented input in games.
I hope this has been easy to follow. Although it took some time to explain the algorithm, this code is very simple at its core. I didn't use any OOP of fancy "Modern C++" features, only simplest code I could imagine.
But you can do whatever you want with your code, and use any modern features you like, for example:
- Use
std::function
to store callbacks, so you could store capturing lambdas there; - Use smart pointers where I used raw pointers, it's your choice. I use raw pointers hovewer because I do not use heap for storing anything in the UI;
- Cache values, like recompute order index only when order of elements is changed, elements are added or deleted;
Positive aspects of such an approach are:
- The processor does not do busy work by moving events all over every frame;
- Implementation is very simple;
Negative sides are:
- It's hard to add new types of behaviours on the user side for now. But even with that limitation, practically any UI element could be implemented using those two behaviours.
In the next part I will make a query mechanism and try to separate shapes from groups, so stay tuned!
- Immediate-Mode Graphical User Interfaces - 2005: https://youtu.be/Z1qyvQsjK5Y
- Data oriented input in games: https://gist.github.com/JustSlavic/0f3570dec2d738aa6a4b624c7fed5000
Part 3: https://gist.github.com/JustSlavic/ef2edbb5476b908e3f08bf6c0a10c3ee