If you've ever watched Casey Muratori's original ImGui video you may be familiar with this approach to hit testing in an immediate mode UI:
void ui_set_next_hot(UI_ID id)
{
ui->next_hot = id;
}
void ui_end_frame()
{
ui->hot = ui->next_hot;
ui->next_hot = 0;
}
Where during the frame when the UI is being built, you call ui_set_next_hot
if a widget is being hovered:
bool ui_button(Rect2 rect, String title)
{
bool result = false;
UI_ID id = ui_id(title);
if (ui_mouse_in_rect(rect))
{
ui_set_next_hot(id);
}
if (ui_is_hot(id))
{
if (ui_left_mouse_clicked())
{
result = true;
}
}
/* draw button */
return result;
}
This works by accepting a one frame delay on registering that a widget is hovered, which is generally not a problem for the user because they can't click a button until they've seen it rendered anyway.
One thing you may eventually want in your UI is the ability to sort widgets separately from the code order:
UI_ID popup_id = ui_id("my_popup");
if (ui_button("Click me for a Pop-Up!"))
{
ui_open_popup(popup_id);
}
if (ui_popup_begin(popup_id))
{
ui_text("This popped up all of a sudden!");
ui_popup_end();
}
The popup and its contents should draw on top of the rest of the UI. The most straightforward way to implement this is something like this:
void ui_push_layer(int depth)
{
ui->current_layer += depth;
}
void ui_pop_layer(int depth)
{
ui->current_layer -= depth;
}
bool ui_popup_begin(UI_ID id)
{
bool is_open = ui_popup_is_open(id);
if (is_open)
{
ui_push_layer(2);
}
return is_open;
}
void ui_popup_end()
{
ui_pop_layer(1);
/* draw popup window background based on size of contents */
ui_pop_layer(1);
}
What will happen is that the UI system will track the current layer, and any draw commands that the widget
emits will be sorted based on the current layer, so the popup's contents will sort on top of
the stuff that came before, and the popup window's background can be drawn based on the size of the
contents because it can be deferred to ui_popup_end
.
When I first implemented this, obviously my next question was how hit testing works with this, but
it turns out the solution is really simple, requiring tracking only a bit of extra state and
modifying ui_set_next_hot
a little:
void ui_set_next_hot(UI_ID id)
{
if (ui->current_layer >= ui->highest_layer_so_far)
{
ui->next_hot = id;
ui->highest_layer_so_far = ui->current_layer;
}
}
void ui_end_frame()
{
ui->hot = ui->next_hot;
ui->next_hot = 0;
ui->current_layer = 0; // reset layer back to the bottom
}
Now widgets are only elegible to be hot if their layer is greater than or equal to the layer of the previous next_hot
widget, which achieves the goal of selecting the widget that sorts the highest at the end of the frame.