Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save TheSandvichMaker/7f8cf93c7c0753be43004a2665f68410 to your computer and use it in GitHub Desktop.
Save TheSandvichMaker/7f8cf93c7c0753be43004a2665f68410 to your computer and use it in GitHub Desktop.
A minimal solution for hit testing sortable UI widgets in an immediate mode API.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment