Skip to content

Custom ImGui UI

ERS uses ImGui to create Graphical User Interface (GUI) application like the ERS Debugger. The same features that were used to create the debugger can also be used in the ERS bindings to create custom applications. This may range from adding custom ImGui widgets to extend the debuggers functionality, to creating fully custom applications from scratch. This page explains how to use ImGui for such purposes.

Tip

If you're not familiar with ImGui or ImPlot, we recommend checking out the Dear ImGui Explorer. It contains live examples with source code of all features of ImGui and ImPlot.

Docking ImGui windows

By default, windows in ImGui "float", meaning they are on top of other UI elements and don't respond to the resizing of the application window or other UI elements. Usually, an application's UI does not consist of different loose windows that are in any position. The ImGui windows should be "docked": anchored in place and resizing with the rest of the application. For example, instead of the model hierarchy floating wherever, it is docked in a left sidebar in the ERS debugger.

ERS Debugger

The ERS Debugger. The model hierarchy is docked in a left sidebar. Other widgets are docked in the center, bottom, and right. The run controls widget is docked at the very top.

When dragging around a window in ImGui, icons will appear on other windows, allowing the dragged window to be docked there.

To dock windows via code, ImGui has "dock nodes". Dock nodes are areas that can be defined with specific sizes and locations in which windows may be docked. For example, the ERS debugger has the following dock nodes:

  • Top bar: the very top in which the run controls are docked.
  • Left sidebar: the side bar to the left where the model hierarchy is docked.
  • Right sidebar: the side bar to the right where the InspectorWidget is docked.
  • Center main: the central area where the visualization is docked.
  • Center bottom: the bottom area where the event timeline and log widgets are docked.

These dock nodes are grouped under a single root dock space. This root dock space is a hidden window which fills the entire application and adds a menu bar to it.

ERS Debugger dock spaces

The ERS Debugger's dock spaces.

The root dock space is necessary to create main menu bar of the application and forms the foundation in which the rest of the application can be docked. Because this setup will be used often, ERS provides a couple of convenience functions to set up this root dock space:

  • Window::BeginRootDockSpace(): begin creation and modification of the root dock space. Returns true on the first frame. The dock space should only be set up on the first frame.
  • Window::GetRootDockSpaceID(): get the ID of the root dock space, which is needed to operate on it.
  • Window::EndRootDockSpace(): finish the creation and modification of the root dock space.

Any setup and modification of the root dock space must be done in between BeginRootDockSpace() and EndRootDockSpace(). Open the note below to see the full setup code for the debugger's docking configuration.

Debugger docking configuration
// If statement makes sure the dock space is only set up on the first frame
if (Window->BeginRootDockSpace())
{
    ImGuiViewport* viewport = ImGui::GetMainViewport();
    RootDockSpaceID         = Window->GetRootDockSpaceID();

    // Optional named IDs so the nodes can be retrieved by name later
    ImGuiID TopBarDockID       = ImGui::GetID("CustomDebugger_TopBarDock");
    ImGuiID MainDockID         = ImGui::GetID("CustomDebugger_MainDock");
    ImGuiID LeftSidebarDockID  = ImGui::GetID("CustomDebugger_LeftSidebarDock");
    ImGuiID CenterDockID       = ImGui::GetID("CustomDebugger_CenterDock");
    ImGuiID RightSidebarDockID = ImGui::GetID("CustomDebugger_RightSidebarDock");
    ImGuiID CenterMainDockID   = ImGui::GetID("CustomDebugger_CenterMainDock");
    ImGuiID CenterBottomDockID = ImGui::GetID("CustomDebugger_CenterBottomDock");

    // Create a full-width top bar for the Run Controls, and the rest below
    ImGuiStyle& style       = ImGui::GetStyle();
    const float topHeightPx = ImGui::GetFrameHeight() + style.ItemSpacing.y + 4.0f;
    const float vpHeight    = viewport->Size.y;
    const float topRatio    = topHeightPx / (vpHeight > 1.0f ? vpHeight : 800.0f);
    ImGui::DockBuilderSplitNode(RootDockSpaceID, ImGuiDir_Up, topRatio, &TopBarDockID, &MainDockID);

    // Lock top bar (no tab bar, no split/resize/undock)
    if (ImGuiDockNode* topNode = ImGui::DockBuilderGetNode(TopBarDockID))
    {
        topNode->LocalFlags |= ImGuiDockNodeFlags_NoTabBar;
        topNode->LocalFlags |= ImGuiDockNodeFlags_NoDockingSplit;
        topNode->LocalFlags |= ImGuiDockNodeFlags_NoResize;
        topNode->LocalFlags |= ImGuiDockNodeFlags_NoDockingOverMe;
        topNode->LocalFlags |= ImGuiDockNodeFlags_NoUndocking;
    }

    // Below: left sidebar, center, right sidebar
    ImGui::DockBuilderSplitNode(MainDockID, ImGuiDir_Left, 0.10f, &LeftSidebarDockID, &CenterDockID);
    ImGui::DockBuilderSplitNode(CenterDockID, ImGuiDir_Right, 0.10f / 0.90f, &RightSidebarDockID, &CenterDockID);

    // Main
    ImGui::DockBuilderSplitNode(CenterDockID, ImGuiDir_Up, 0.75f, &CenterMainDockID, &CenterBottomDockID);

    // Top bar
    ImGui::DockBuilderDockWindow("Run Controls", TopBarDockID);
    // Left sidebar
    ImGui::DockBuilderDockWindow("Model Hierarchy", LeftSidebarDockID);
    // Right sidebar
    ImGui::DockBuilderDockWindow("Inspector", RightSidebarDockID);
    // Main
    ImGui::DockBuilderDockWindow("Visualization", CenterMainDockID);
    ImGui::DockBuilderDockWindow("Model Structure", CenterMainDockID);
    ImGui::DockBuilderDockWindow("License Manager", CenterMainDockID);
    // Main-Bottom
    ImGui::DockBuilderDockWindow("Log", CenterBottomDockID);
    ImGui::DockBuilderDockWindow("Event Timeline", CenterBottomDockID);
    ImGui::DockBuilderDockWindow("Model Progression", CenterBottomDockID);

    ImGui::DockBuilderFinish(RootDockSpaceID);
}

// Widgets used here...

Window->EndRootDockSpace();
// If statement makes sure the dock space is only set up on the first frame
if (Window.BeginRootDockSpace())
{
    ImGuiViewportPtr viewport = ImGui.GetMainViewport();
    uint rootDockSpaceID      = Window.RootDockSpaceID;

    // Optional named IDs so the nodes can be retrieved by name later
    uint topBarDockID       = ImGui.GetID("CustomDebugger_TopBarDock");
    uint mainDockID         = ImGui.GetID("CustomDebugger_MainDock");
    uint leftSideBarDockID  = ImGui.GetID("CustomDebugger_LeftSideBarDock");
    uint centerDockID       = ImGui.GetID("CustomDebugger_CenterDock");
    uint rightSideBarDockID = ImGui.GetID("CustomDebugger_RightSideBarDock");
    uint centerMainDockID   = ImGui.GetID("CustomDebugger_CenterMainDock");
    uint centerBottomDockID = ImGui.GetID("CustomDebugger_CenterBottomDock");

    // Create a full-width top bar for the Run Controls, and the rest below
    ImGuiStylePtr style = ImGui.GetStyle();
    float topHeightPx   = ImGui.GetFrameHeight() + style.ItemSpacing.Y + 4.0f;
    float vpHeight      = viewport.Size.Y;
    float topRatio      = topHeightPx / (vpHeight > 1.0f ? vpHeight : 800.0f);
    ImGui.DockBuilderSplitNode(rootDockSpaceID, ImGuiDir.Up, topRatio, out topBarDockID, out mainDockID);

    // Lock top bar (no tab bar, no split/resize/undock)
    ImGuiDockNodePtr topNode = ImGui.DockBuilderGetNode(topBarDockID);
    if (topNode.ID > 0)
    {
        topNode.LocalFlags |= (ImGuiDockNodeFlags)ImGuiDockNodeFlagsPrivate.ImGuiDockNodeFlags_NoTabBar;
        topNode.LocalFlags |= ImGuiDockNodeFlags.NoDockingSplit;
        topNode.LocalFlags |= ImGuiDockNodeFlags.NoResize;
        topNode.LocalFlags |= ImGuiDockNodeFlags.NoDockingOverCentralNode;
        topNode.LocalFlags |= ImGuiDockNodeFlags.NoUndocking;
    }

    // Below: left sidebar, center, right sidebar
    ImGui.DockBuilderSplitNode(mainDockID, ImGuiDir.Left, 0.10f, out leftSideBarDockID, out centerDockID);
    ImGui.DockBuilderSplitNode(centerDockID, ImGuiDir.Right, 0.10f / 0.90f, out rightSideBarDockID, out centerDockID);

    // Main
    ImGui.DockBuilderSplitNode(centerDockID, ImGuiDir.Up, 0.75f, out centerMainDockID, out centerBottomDockID);

    // Top bar
    ImGui.DockBuilderDockWindow("Run Controls", topBarDockID);
    // Left sidebar
    ImGui.DockBuilderDockWindow("Model Hierarchy", leftSideBarDockID);
    // Right sidebar
    ImGui.DockBuilderDockWindow("Inspector", rightSideBarDockID);
    // Main
    ImGui.DockBuilderDockWindow("Visualization", centerMainDockID);
    ImGui.DockBuilderDockWindow("Model Structure", centerMainDockID);
    ImGui.DockBuilderDockWindow("License Manager", centerMainDockID);
    // Main-Bottom
    ImGui.DockBuilderDockWindow("Log", centerBottomDockID);
    ImGui.DockBuilderDockWindow("Event Timeline", centerBottomDockID);
    ImGui.DockBuilderDockWindow("Model Progression", centerBottomDockID);

    ImGui.DockBuilderFinish(rootDockSpaceID);
}

// Widgets used here...

Window.EndRootDockSpace();

Adding custom widgets to the debugger

The ERS Debugger can be expanded by adding custom ImGui widgets. This can done in two ways:

  • Using the renderUI callback of the Debugger::Run function.
  • Setting up the debugger manually (mimicking the Run function) and adding the desired changes.

The first method is the simplest and should be sufficient for most use cases. The second method can be used in more advanced scenarios. To apply the second method, copy the implementation inside the Run method (most code editor have an "Inspect implementation" feature), and add code to expand its functionality.

To apply the first method, use the Run function as before by providing a model container as the first parameter, and one or two render functions for the second and third parameters (the second parameter for 2D rendering and the third for 3D rendering).

internal class Program
{
    private static bool customWidgetsInitialized = false;

    public static void RenderUI(Debugger debugger)
    {
        // Only call the docking function once,
        // otherwise it will be re-docked each frame and the user won't be able to move it.
        if (!customWidgetsInitialized)
        {
            // Dock the custom widget inside the "center main" dock node
            ImGui.DockBuilderDockWindow("Custom widget", debugger.CenterMainDockID);
            customWidgetsInitialized = true;
        }

        // Custom widget inside a window to be able to dock it
        ImGui.Begin("Custom widget");
        ImGui.Text("Custom widget code goes here");
        ImGui.End();
    }

    static void Main(string[] args)
    {
        ERS.Initialize();

        ModelContainer modelContainer = ModelContainer.Create();
        Ers.Debugger.Run(modelContainer, null, null, RenderUI);

        ERS.Uninitialize();
    }
}

The code above creates a new ImGui window called "Custom widget". Inside this window, the custom widget can be implemented. In this example, only some text is shown in the body of the window. Only on the first frame is the window docked inside the "center main" dock node of the debugger. If DockBuilderDockWindow would be called every frame, the window would re-dock each frame, in which case the user would not be able to move the window at all.

Creating a custom application

Sometimes adding more features on top of the ERS Debugger is not good enough: a completely custom interface totally different from the debugger may be necessary. For these cases, ERS allows creating an empty window on top of which a custom application can be built using ImGui. In fact, the debugger is built this way too.

Below is an example for creating an empty window and adding ImGui elements to it. The example adds the following features:

  • A main menu bar for the entire application.
    • A menu button to close the application (File \(\rightarrow\) Exit).
    • A menu where the color theme can be changed (View \(\rightarrow\) Theme).
  • Two docked windows that split the application's space in half.
    • "Window A" on the left with a text widget inside it.
    • "Window B" on the right with a slider widget to adjust the value of a float.
internal class Program
{
    private static bool dockingInitialized = false;
    private static bool isDarkTheme = true;
    private static float floatValue = 0.5f;

    static void Main(string[] args)
    {
        ERS.Initialize();

        Window window = new("My custom application");
        // Apply the ERS dark theme by default
        ErsImGui.StyleColorsErsDark();
        while (!window.WantsClose)
        {
            window.BeginFrame();
            window.BeginRootDockSpace();

            // Add the main menu bar for the entire application
            if (ImGui.BeginMainMenuBar())
            {
                // First menu bar menu
                if (ImGui.BeginMenu("File"))
                {
                    if (ImGui.MenuItem("Exit"))
                    {
                        // Setting this to true breaks the update loop
                        window.WantsClose = true;
                    }
                    ImGui.EndMenu();
                }

                // Second menu bar menu
                if (ImGui.BeginMenu("View"))
                {
                    // Theme selection menu
                    if (ImGui.BeginMenu("Theme"))
                    {
                        // Dark and light options
                        if (ImGui.MenuItem("Dark", "", isDarkTheme))
                        {
                            ErsImGui.StyleColorsErsDark();
                            isDarkTheme = true;
                        }
                        if (ImGui.MenuItem("Light", "", !isDarkTheme))
                        {
                            ErsImGui.StyleColorsErsLight();
                            isDarkTheme = false;
                        }
                        ImGui.EndMenu();
                    }
                    ImGui.EndMenu();
                }
                ImGui.EndMainMenuBar();
            }

            // Set up the docking configuration once
            if (!dockingInitialized)
            {
                // Split the root dock space 50/50 between a left and right node
                uint leftDockID, rightDockID;
                ImGui.DockBuilderSplitNode(window.RootDockSpaceID, ImGuiDir.Left, 0.5f, out leftDockID, out rightDockID);

                ImGui.DockBuilderDockWindow("Window A", leftDockID);
                ImGui.DockBuilderDockWindow("Window B", rightDockID);
                dockingInitialized = true;
            }

            // Add the ImGui windows
            ImGui.Begin("Window A");
            ImGui.Text("Text in window A");
            ImGui.End();

            ImGui.Begin("Window B");
            ImGui.SliderFloat("Adjustable float", ref floatValue, 0.0f, 1.0f);
            ImGui.End();

            // Potentially more ImGui and ERS code here...

            window.EndRootDockSpace();
            window.EndFrame();
        }

        ERS.Uninitialize();
    }
}

Custom ImGui application

The result of the custom ImGui application code. The user is hovering over the button to switch to the light theme in the menu bar (View \(\rightarrow\) Theme \(\rightarrow\) Light). The body of the application is divided equally amongst Window A (below the opened menu item) and Window B.

This example is just the beginning of what a custom application can do. Inside the application, more UI, the pre-built ERS widgets, or any anything else can be added.

Was this page helpful?