ocornut / imgui

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
MIT License
61.53k stars 10.36k forks source link

Force docked windows to stay in their original dock while allowing layout changes #7053

Closed Per-Morten closed 12 months ago

Per-Morten commented 1 year ago

Version/Branch of Dear ImGui: Version: 1.90.0 Branch: docking

Dear ImGui 1.90.0 (19000)
--------------------------------
sizeof(size_t): 8, sizeof(ImDrawIdx): 2, sizeof(ImDrawVert): 20
define: __cplusplus=199711
define: IMGUI_DISABLE_OBSOLETE_FUNCTIONS
define: IMGUI_DISABLE_OBSOLETE_KEYIO
define: _WIN32
define: _WIN64
define: _MSC_VER=1937
define: _MSVC_LANG=201402
define: IMGUI_HAS_VIEWPORT
define: IMGUI_HAS_DOCK
--------------------------------
io.BackendPlatformName: imgui_impl_win32
io.BackendRendererName: imgui_impl_dx11
io.ConfigFlags: 0x00000043
 NavEnableKeyboard
 NavEnableGamepad
 DockingEnable
io.ConfigViewportsNoDecoration
io.ConfigDockingWithShift
io.ConfigDockingTransparentPayload
io.ConfigInputTextCursorBlink
io.ConfigWindowsResizeFromEdges
io.ConfigMemoryCompactTimer = 60.0
io.BackendFlags: 0x00001C0E
 HasMouseCursors
 HasSetMousePos
 PlatformHasViewports
 HasMouseHoveredViewport
 RendererHasVtxOffset
 RendererHasViewports
--------------------------------
io.Fonts: 1 fonts, Flags: 0x00000000, TexSize: 512,64
io.DisplaySize: 1920.00,1009.00
io.DisplayFramebufferScale: 1.00,1.00
--------------------------------
style.WindowPadding: 8.00,8.00
style.WindowBorderSize: 1.00
style.FramePadding: 4.00,3.00
style.FrameRounding: 0.00
style.FrameBorderSize: 0.00
style.ItemSpacing: 8.00,4.00
style.ItemInnerSpacing: 4.00,4.00

Back-end/Renderer/Compiler/OS Back-ends: imgui_impl_win32 + imgui_impl_dx11 Operating System: Windows 11 x64

My Issue/Question: I'm creating a "file inspector" debug tool, where each root window (Root A and Root B in the gifs) is a "file". Each of these files can have multiple items (Item A->Item D in the gifs) which is an object within said file. I want to be able to split/layout all the different objects within a file, but I never want those those objects to be docked to anything other than their original windows (to avoid confusion as to what file an object belongs to).

Basically, to summarize the functionality I want (see gif): I want Item A and Item B (and any other items I would add to Root A) to be able to be split into all endless combinations that docking allow, but they must always be docked somewhere in their original dockspace in the Root A window.

I'm wondering what's the best way to achieve this functionality?

My current approach (see code snippet) is to only allow layout changes (undocking) when the user is holding the shift key. If the user lets go of the shift key and the window is not docked to its original dock space I force it back there. I'm using StateStorage to track whether or not the window needs to be docked in its original dock space.

This approach works, but has some issues: Firstly, it doesn't work if ImGuiWindowClass::DockingAlwaysTabBar is set to true. Because then the item windows creates their own docking space when they are undocked from the root windows.

Secondly, If you have an undocked root, are in the process of moving an item window and let go of shift, the root is moved to where your mouse cursor is (see second gif showing the difference in behaviour).

Thirdly, (not very important) This approach doesn't feel super "intended" but more "workaroundy". Using state storage to store "single frame behaviour" isn't something I normally need to do in ImGui. Now every begin/end need to have this "little dance" of using state storage that's modified inside the window. Note, so far I've been able to make all my tools with a minimal amount of abstractions/utilities on top of Dear ImGui, so this feeling might just be lack of more complex experience.

To somewhat repeat my question: Is there any better way to achieve the functionality that I want?

Screenshots/Video Gif 1: Showing current & desired behaviour BasicFunctionality

Gif 2: Showing the broken move behaviour BrokenMove

Standalone, minimal, complete and verifiable example:

    if (ImGui::Begin("Root A"))
    {
        // Create window class so we can't be docked to docking zones in
        // other windows I assume this is because the window_class will have
        // a ClassId that's affected by the ID of Root A?
        auto window_class = ImGuiWindowClass();
        window_class.ClassId = ImGui::GetID("##WindowClass");
        window_class.DockingAllowUnclassed = false;

        static bool always_tab_bar = false;
        ImGui::Checkbox("Always tab bar", &always_tab_bar);
        window_class.DockingAlwaysTabBar = always_tab_bar;

        // Already have io.ConfigDockingWithShift = true. Adding to that
        // "restriction" by not allowing any form of docking or undocking
        // unless shift is held.
        auto flags = ImGui::IsKeyDown(ImGuiKey_LeftShift)
            ? ImGuiDockNodeFlags_None
            : ImGuiDockNodeFlags_NoUndocking;

        auto dockspace_id = ImGui::DockSpace(ImGui::GetID("##DockSpaceID"), ImVec2(0.0f, 0.0f), flags, &window_class);

        {
            ImGui::SetNextWindowClass(&window_class);

            auto must_be_parented_id = ImGui::GetID("##MustBeParentedA");
            auto must_be_parented = ImGui::GetStateStorage()->GetBool(must_be_parented_id, true);

            if (must_be_parented)
                ImGui::SetNextWindowDockID(dockspace_id);

            // Create custom name for this window so we don't append to window
            // docked in Root B
            char window_a_heading[256];
            std::sprintf(window_a_heading, "Item A##%d", dockspace_id);
            if (ImGui::Begin(window_a_heading))
            {
                must_be_parented = ImGui::GetWindowDockID() == 0 && !ImGui::IsKeyDown(ImGuiKey_LeftShift);
            }
            ImGui::End();
            ImGui::GetStateStorage()->SetBool(must_be_parented_id, must_be_parented);
        }

        // Copy paste from scope above
        {
            ImGui::SetNextWindowClass(&window_class);

            auto must_be_parented_id = ImGui::GetID("##MustBeParentedB");
            auto must_be_parented = ImGui::GetStateStorage()->GetBool(must_be_parented_id, true);

            if (must_be_parented)
                ImGui::SetNextWindowDockID(dockspace_id);

            // Create custom name for this window so we don't append to window
            // docked in Root B
            char window_a_heading[256];
            std::sprintf(window_a_heading, "Item B##%d", dockspace_id);
            if (ImGui::Begin(window_a_heading))
            {
                must_be_parented = ImGui::GetWindowDockID() == 0 && !ImGui::IsKeyDown(ImGuiKey_LeftShift);
            }
            ImGui::End();
            ImGui::GetStateStorage()->SetBool(must_be_parented_id, must_be_parented);
        }
    }
    ImGui::End();

    // Copy paste from Root A with changed names and removed comments
    if (ImGui::Begin("Root B"))
    {
        auto window_class = ImGuiWindowClass();
        window_class.ClassId = ImGui::GetID("##WindowClass");
        window_class.DockingAllowUnclassed = false;

        // Ask about this
        static bool always_tab_bar = false;
        ImGui::Checkbox("Always tab bar", &always_tab_bar);
        window_class.DockingAlwaysTabBar = always_tab_bar;

        auto flags = ImGui::IsKeyDown(ImGuiKey_LeftShift)
            ? ImGuiDockNodeFlags_None
            : ImGuiDockNodeFlags_NoUndocking;

        auto dockspace_id = ImGui::DockSpace(ImGui::GetID("##DockSpaceID"), ImVec2(0.0f, 0.0f), flags, &window_class);

        {
            ImGui::SetNextWindowClass(&window_class);

            auto must_be_parented_id = ImGui::GetID("##MustBeParentedC");
            auto must_be_parented = ImGui::GetStateStorage()->GetBool(must_be_parented_id, true);

            if (must_be_parented)
                ImGui::SetNextWindowDockID(dockspace_id);

            char window_c_heading[256];
            std::sprintf(window_c_heading, "Item C##%d", dockspace_id);
            if (ImGui::Begin(window_c_heading))
            {
                must_be_parented = ImGui::GetWindowDockID() == 0 && !ImGui::IsKeyDown(ImGuiKey_LeftShift);
            }
            ImGui::End();
            ImGui::GetStateStorage()->SetBool(must_be_parented_id, must_be_parented);
        }

        // Copy paste from scope above
        {
            ImGui::SetNextWindowClass(&window_class);

            auto must_be_parented_id = ImGui::GetID("##MustBeParentedD");
            auto must_be_parented = ImGui::GetStateStorage()->GetBool(must_be_parented_id, true);

            if (must_be_parented)
                ImGui::SetNextWindowDockID(dockspace_id);

            char window_d_heading[256];
            std::sprintf(window_d_heading, "Item D##%d", dockspace_id);
            if (ImGui::Begin(window_d_heading))
            {
                must_be_parented = ImGui::GetWindowDockID() == 0 && !ImGui::IsKeyDown(ImGuiKey_LeftShift);
            }
            ImGui::End();
            ImGui::GetStateStorage()->SetBool(must_be_parented_id, must_be_parented);
        }
    }
    ImGui::End();
Per-Morten commented 1 year ago

Thirdly, (not very important) This approach doesn't feel super "intended" but more "workaroundy". Using state storage to store "single frame behaviour" isn't something I normally need to do in ImGui. Now every begin/end need to have this "little dance" of using state storage that's modified inside the window. Note, so far I've been able to make all my tools with a minimal amount of abstractions/utilities on top of Dear ImGui, so this feeling might just be lack of more complex experience.

So I at least figured out that I can avoid using state storage by using begin/end to get into the context of the window https://github.com/ocornut/imgui/wiki/Tips. Only really solves the third issue (easier to abstract away in a function). But, is this allowed, am I allowed to set the window dock id after I've already created the window once?

{
    ImGui::SetNextWindowClass(&window_class);

    char window_a_heading[256];
    std::sprintf(window_a_heading, "Item A##%d", dockspace_id);

    // Create window just to check if we must be reparented.
    bool must_be_parented = false;
    if (ImGui::Begin(window_a_heading))
        must_be_parented = ImGui::GetWindowDockID() == 0 && !ImGui::IsKeyDown(ImGuiKey_LeftShift);
    ImGui::End();

    if (must_be_parented)
        ImGui::SetNextWindowDockID(dockspace_id);

    if (ImGui::Begin(window_a_heading))
    {
        // ...
    }
    ImGui::End();
}
Per-Morten commented 1 year ago

After some experimentation I finally found an approach that gives me the functionality I wanted. In case anyone is trying to do something similar, this is what I ended up with:

ImGuiID ForcedDockSpace(ImGuiID id, ImGuiWindowClass* window_class, const ImVec2& size = ImVec2(0, 0), ImGuiDockNodeFlags flags = 0)
{
    window_class->DockingAllowUnclassed = false;

    if (!ImGui::IsKeyDown(ImGuiKey_LeftShift))
        flags |= ImGuiDockNodeFlags_NoUndocking;

    return ImGui::DockSpace(id, ImVec2(0.0f, 0.0f), flags, window_class);
}

bool BeginForceDockedWindow(const char* name, ImGuiID dockspace_id, const ImGuiWindowClass* window_class = NULL, bool* p_open = NULL, ImGuiWindowFlags flags = 0)
{
    if (window_class)
        ImGui::SetNextWindowClass(window_class);

    // Need an id for state storage. Can't use window->GetID, because even
    // if window has been created before it might have had its memory
    // compacted, meaning that the IDStack is empty. IDStack is recreated in
    // ImGui::Begin. Instead of recreating it we just create our own id we
    // can use in state storage. State storage is local per window, so we
    // shouldn't need this id to be seeded by the window.
    const ImGuiID check_force_dock_on_frame_id = ImHashStr("ForceDockedWindow::CheckForceDockOnFrame");
    const ImGuiID drag_drop_previous_frame_id = ImHashStr("ForceDockedWindow::DragDropPreviousFrame");

    bool force_dock = false;
    ImGuiWindow* window = ImGui::FindWindowByName(name);
    if (window)
    {
        int check_on_frame = window->StateStorage.GetInt(check_force_dock_on_frame_id, 0);

        if (ImGui::GetFrameCount() == check_on_frame)
        {
            ImGuiContext* ctx = ImGui::GetCurrentContext();
            ImGuiDockNode* parent_node = ImGui::DockContextFindNodeByID(ctx, dockspace_id);
            force_dock = !ImGui::DockNodeIsInHierarchyOf(window->DockNode, parent_node);

            // If we let go of shift but not the mouse button while in drag
            // and drop mode we'll move the root window to the position of
            // the mouse unless we clear the ActiveID
            if (ctx->ActiveIdWindow == window)
                ImGui::ClearActiveID();
        }
    }

    ImGui::SetNextWindowDockID(dockspace_id, force_dock ? ImGuiCond_Always : ImGuiCond_FirstUseEver);

    bool begin_result = ImGui::Begin(name, p_open, flags);
    window = ImGui::GetCurrentWindow();

    bool* drag_drop_previous_frame = window->StateStorage.GetBoolRef(drag_drop_previous_frame_id, false);
    bool drag_drop_current_frame = ImGui::IsDragDropActive();

    bool ended_docking_this_frame = *drag_drop_previous_frame && !drag_drop_current_frame;
    *drag_drop_previous_frame = drag_drop_current_frame;

    if (ended_docking_this_frame)
        window->StateStorage.SetInt(check_force_dock_on_frame_id, ImGui::GetFrameCount() + 1);

    return begin_result;
}

void EndForceDockedWindow()
{
    ImGui::End();
}

Which can be used like this:

    if (ImGui::Begin("Root A"))
    {
        ImGuiWindowClass window_class;
        window_class.ClassId = ImGui::GetID("##WindowClass");
        window_class.DockingAllowUnclassed = false;

        auto dockspace_id = ForcedDockSpace(ImGui::GetID("##ForcedDockSpace"), &window_class);

        if (BeginForceDockedWindow("Child A", dockspace_id, &window_class))
        {

        }
        EndForceDockedWindow();

        if (BeginForceDockedWindow("Child B", dockspace_id, &window_class))
        {

        }
        EndForceDockedWindow();

        if (BeginForceDockedWindow("Child C", dockspace_id, &window_class))
        {

        }
        EndForceDockedWindow();
    }
    ImGui::End();
ocornut commented 12 months ago

Sorry for not looking into this before, quite swamped in things to do.

Firstly, it doesn't work if ImGuiWindowClass::DockingAlwaysTabBar is set to true. Because then the item windows creates their own docking space when they are undocked from the root windows.

This statement may be incorrect, as my own demo for it (https://github.com/ocornut/imgui/issues/6487#issuecomment-1580720798) doesn't exhibit the issue. Just tested now and activated DockingAlwaysTabBar = true and the filtering worked.

If I understand correctly, your main issue is that you never want those "Item" windows to be kept floating outside, unless they being dragged?

If you only want to prevent them from being docked into another "Root" window, then SetNextWindowClass() with a ClassId value should be enough and anything else is extraneous.

If you absolutely need to "reparent" the "Item" window to their "Root" window if they are dropped outside, what I would do is the following:

// Create window class so we can't be docked to docking zones in // other windows I assume this is because the window_class will have // a ClassId that's affected by the ID of Root A? window_class.ClassId = ImGui::GetID("##WindowClass");

For info: that's correct. The ID stack of each window include the window name at bottom of the stack, so here your class id would actually be == ImHashStr("Root A##WindowClass");

Per-Morten commented 12 months ago

Sorry for not looking into this before, quite swamped in things to do.

No worries, I closed the issue mainly because I found a working solution. It's not very important for me to improve on that solution unless there's a much simpler way of doing it (or one that doesn't require accessing imgui_internal.h, so there's less chance of it breaking in updates). So feel free to leave this issue closed and not following up on it further :)

Firstly, it doesn't work if ImGuiWindowClass::DockingAlwaysTabBar is set to true. Because then the item windows creates their own docking space when they are undocked from the root windows.

This statement may be incorrect, as my own demo for it (#6487 (comment)) doesn't exhibit the issue. Just tested now and activated DockingAlwaysTabBar = true and the filtering worked.

I see I forgot to include a gif of that behaviour but it is present when I run the code I originally posted in the DirectX11 example. (only modified io.ConfigDockingWithShift to be true, since that's kinda required for the original code to work as intended.
BrokenAlwaysTabBar

If I understand correctly, your main issue is that you never want those "Item" windows to be kept floating outside, unless they being dragged?

Yes. Essentially, the user can layout (split, merge, etc) those "Item" windows as much as they want inside of a specific dock node/window.

If you only want to prevent them from being docked into another "Root" window, then SetNextWindowClass() with a ClassId value should be enough and anything else is extraneous.

If you absolutely need to "reparent" the "Item" window to their "Root" window if they are dropped outside, what I would do is the following:

* Setup the regular `SetNextWindowClass()` constraint above.

* Call `GetWindowDockID()` after Begin() and IF the value is not 0, record it somewhere (each item records it's last non-0 dock id).

* You can call `IsItemActive()` after Begin() to tell if a window is being dragged. When that's false and `GetWindowDockID() == 0`) you may restore the last valid value using `DockBuilderDockWindow()` or `SetNextWindowDockID() + re Begin()`.

So, something like this?

bool 
BeginForceDockedWindow(const char* name, ImGuiID root_dockspace_id, const ImGuiWindowClass* window_class = nullptr, bool* p_open = nullptr, ImGuiWindowFlags flags = 0)
{
    if (window_class)
        ImGui::SetNextWindowClass(window_class);

    ImGui::Begin(name, p_open, flags);
    const bool is_window_dragged = ImGui::IsItemActive();
    const int current_dock_id = ImGui::GetWindowDockID();
    int* previous_valid_dock_id = ImGui::GetStateStorage()->GetIntRef(ImGui::GetID("PreviousValidDockID"), root_dockspace_id);

    // Validate current_dock_id is within hierarchy of root_dockspace_id
    bool is_in_correct_hierarchy = false;
    ImGuiDockNode* itr = ImGui::GetWindowDockNode();
    while (itr)
    {
        if (itr->ID == root_dockspace_id)
        {
            is_in_correct_hierarchy = true;
            break;
        }
        itr = itr->ParentNode;
    }

    *previous_valid_dock_id = is_in_correct_hierarchy ? current_dock_id : root_dockspace_id;

    const int valid_dock_id = *previous_valid_dock_id;
    ImGui::End();

    if (!is_window_dragged && !is_in_correct_hierarchy)
        ImGui::SetNextWindowDockID(valid_dock_id);

    bool ret = ImGui::Begin(name, p_open, flags);
    return ret;
}

void EndForceDockedWindow()
{
    ImGui::End();
}

// Use
if (ImGui::Begin("Root A"))
{
    auto window_class = ImGuiWindowClass();
    window_class.ClassId = ImGui::GetID("##WindowClass");
    window_class.DockingAllowUnclassed = false;

    static bool always_tab_bar = false;
    ImGui::Checkbox("Always tab bar", &always_tab_bar);
    window_class.DockingAlwaysTabBar = always_tab_bar;

    auto dockspace_id = ImGui::DockSpace(ImGui::GetID("##DockSpaceID"), ImVec2(0.0f, 0.0f), 0, &window_class);

    if (BeginForceDockedWindow("Item A", dockspace_id, &window_class))
    {

    }
    EndForceDockedWindow();

    if (BeginForceDockedWindow("Item B", dockspace_id, &window_class))
    {

    }
    EndForceDockedWindow();
}
ImGui::End();

if (ImGui::Begin("Root B"))
{
    auto window_class = ImGuiWindowClass();
    window_class.ClassId = ImGui::GetID("##WindowClass");
    window_class.DockingAllowUnclassed = false;

    static bool always_tab_bar = false;
    ImGui::Checkbox("Always tab bar", &always_tab_bar);
    window_class.DockingAlwaysTabBar = always_tab_bar;

    auto dockspace_id = ImGui::DockSpace(ImGui::GetID("##DockSpaceID"), ImVec2(0.0f, 0.0f), 0, &window_class);

    if (BeginForceDockedWindow("Item C", dockspace_id, &window_class))
    {

    }
    EndForceDockedWindow();

    if (BeginForceDockedWindow("Item D", dockspace_id, &window_class))
    {

    }
    EndForceDockedWindow();
}
ImGui::End();

Somewhat an improvement, less state, and I don't need to require the user to hold shift to drag & drop windows etc. However, when DockingAlwaysTabBar is set to true, I trigger an assert in Begin, line 6840.

    // Parent window is latched only on the first call to Begin() of the frame, so further append-calls can be done from a different window stack
    ImGuiWindow* parent_window_in_stack = (window->DockIsActive && window->DockNode->HostWindow) ? window->DockNode->HostWindow : g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back().Window;
    ImGuiWindow* parent_window = first_begin_of_the_frame ? ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup)) ? parent_window_in_stack : NULL) : window->ParentWindow;
    IM_ASSERT(parent_window != NULL || !(flags & ImGuiWindowFlags_ChildWindow));

Parent window ends up being null, while ImGuiWindowFlags_ChildWindow is set on flags (set from ImGui::Begin, I'm not setting any window flags).

ocornut commented 12 months ago

You shouldn’t have to validate the hierarchy, the ClassId system should do that for you.

Per-Morten commented 11 months ago

Ok, then I think I'm misunderstanding something, or we are potentially talking past each other. If I understand your approach correctly, and drop the hierarchy validation, then I'm left with this:

bool
BeginForceDockedWindow(const char* name, ImGuiID root_dockspace_id, const ImGuiWindowClass* window_class = nullptr, bool* p_open = nullptr, ImGuiWindowFlags flags = 0)
{
    if (window_class)
        ImGui::SetNextWindowClass(window_class);

    ImGui::Begin(name, p_open, flags);
    const bool is_item_active = ImGui::IsItemActive();
    const int current_dock_id = ImGui::GetWindowDockID();
    int* previous_valid_dock_id = ImGui::GetStateStorage()->GetIntRef(ImGui::GetID("PreviousValidDockID"), root_dockspace_id);

    if (current_dock_id != 0)
        *previous_valid_dock_id = current_dock_id;
    ImGui::End();

    if (is_item_active == false && current_dock_id == 0)
        ImGui::SetNextWindowDockID(*previous_valid_dock_id);

    bool ret = ImGui::Begin(name, p_open, flags);
    ImGui::Text("%d", ImGui::GetWindowDockID()); // debug
    return ret;
}

void EndForceDockedWindow()
{
    ImGui::End();
}

// Usage
if (ImGui::Begin("Root A"))
{
    auto window_class = ImGuiWindowClass();
    window_class.ClassId = ImGui::GetID("##WindowClass");
    window_class.DockingAllowUnclassed = false;

    static bool always_tab_bar = false;
    ImGui::Checkbox("Always tab bar", &always_tab_bar);
    window_class.DockingAlwaysTabBar = always_tab_bar;

    auto dockspace_id = ImGui::DockSpace(ImGui::GetID("##DockSpaceID"), ImVec2(0.0f, 0.0f), 0, &window_class);

    if (BeginForceDockedWindow("Item A", dockspace_id, &window_class))
    {

    }
    EndForceDockedWindow();

    if (BeginForceDockedWindow("Item B", dockspace_id, &window_class))
    {

    }
    EndForceDockedWindow();
}
ImGui::End();

if (ImGui::Begin("Root B"))
{
    auto window_class = ImGuiWindowClass();
    window_class.ClassId = ImGui::GetID("##WindowClass");
    window_class.DockingAllowUnclassed = false;

    static bool always_tab_bar = false;
    ImGui::Checkbox("Always tab bar", &always_tab_bar);
    window_class.DockingAlwaysTabBar = always_tab_bar;

    auto dockspace_id = ImGui::DockSpace(ImGui::GetID("##DockSpaceID"), ImVec2(0.0f, 0.0f), 0, &window_class);

    if (BeginForceDockedWindow("Item C", dockspace_id, &window_class))
    {

    }
    EndForceDockedWindow();

    if (BeginForceDockedWindow("Item D", dockspace_id, &window_class))
    {

    }
    EndForceDockedWindow();
}
ImGui::End();

That works for the situation where the item windows are docked on the same node. CorrectReparent

But it does not work when I've created the vertical split before I pull Item C out to "float". At the end of the animation Item C should have snapped back like it did in the previous gif, (or snapped back to it's vertical split layout, but I assume that's more complicated, so it's not important for me) BrokenSplit

And it does not work when always tab bar is enabled. BrokenAlwaysTabBar

MCVE with win32 dx11 example

```cpp #include "imgui.h" #include "imgui_impl_win32.h" #include "imgui_impl_dx11.h" #include #include bool BeginForceDockedWindow(const char* name, ImGuiID root_dockspace_id, const ImGuiWindowClass* window_class = nullptr, bool* p_open = nullptr, ImGuiWindowFlags flags = 0) { if (window_class) ImGui::SetNextWindowClass(window_class); ImGui::Begin(name, p_open, flags); const bool is_item_active = ImGui::IsItemActive(); const int current_dock_id = ImGui::GetWindowDockID(); int* previous_valid_dock_id = ImGui::GetStateStorage()->GetIntRef(ImGui::GetID("PreviousValidDockID"), root_dockspace_id); if (current_dock_id != 0) *previous_valid_dock_id = current_dock_id; ImGui::End(); if (is_item_active == false && current_dock_id == 0) ImGui::SetNextWindowDockID(*previous_valid_dock_id); bool ret = ImGui::Begin(name, p_open, flags); ImGui::Text("%d", ImGui::GetWindowDockID()); // Debug output return ret; } void EndForceDockedWindow() { ImGui::End(); } void NewImplementationTest() { if (ImGui::Begin("Root A")) { auto window_class = ImGuiWindowClass(); window_class.ClassId = ImGui::GetID("##WindowClass"); window_class.DockingAllowUnclassed = false; static bool always_tab_bar = false; ImGui::Checkbox("Always tab bar", &always_tab_bar); window_class.DockingAlwaysTabBar = always_tab_bar; auto dockspace_id = ImGui::DockSpace(ImGui::GetID("##DockSpaceID"), ImVec2(0.0f, 0.0f), 0, &window_class); if (BeginForceDockedWindow("Item A", dockspace_id, &window_class)) { } EndForceDockedWindow(); if (BeginForceDockedWindow("Item B", dockspace_id, &window_class)) { } EndForceDockedWindow(); } ImGui::End(); if (ImGui::Begin("Root B")) { auto window_class = ImGuiWindowClass(); window_class.ClassId = ImGui::GetID("##WindowClass"); window_class.DockingAllowUnclassed = false; static bool always_tab_bar = false; ImGui::Checkbox("Always tab bar", &always_tab_bar); window_class.DockingAlwaysTabBar = always_tab_bar; auto dockspace_id = ImGui::DockSpace(ImGui::GetID("##DockSpaceID"), ImVec2(0.0f, 0.0f), 0, &window_class); if (BeginForceDockedWindow("Item C", dockspace_id, &window_class)) { } EndForceDockedWindow(); if (BeginForceDockedWindow("Item D", dockspace_id, &window_class)) { } EndForceDockedWindow(); } ImGui::End(); } // Dear ImGui: standalone example application for DirectX 11 // Learn about Dear ImGui: // - FAQ https://dearimgui.com/faq // - Getting Started https://dearimgui.com/getting-started // - Documentation https://dearimgui.com/docs (same as your local docs/ folder). // - Introduction, links and more at the top of imgui.cpp // Data static ID3D11Device* g_pd3dDevice = nullptr; static ID3D11DeviceContext* g_pd3dDeviceContext = nullptr; static IDXGISwapChain* g_pSwapChain = nullptr; static UINT g_ResizeWidth = 0, g_ResizeHeight = 0; static ID3D11RenderTargetView* g_mainRenderTargetView = nullptr; // Forward declarations of helper functions bool CreateDeviceD3D(HWND hWnd); void CleanupDeviceD3D(); void CreateRenderTarget(); void CleanupRenderTarget(); LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); // Main code int main(int, char**) { // Create application window //ImGui_ImplWin32_EnableDpiAwareness(); WNDCLASSEXW wc = { sizeof(wc), CS_CLASSDC, WndProc, 0L, 0L, GetModuleHandle(nullptr), nullptr, nullptr, nullptr, nullptr, L"ImGui Example", nullptr }; ::RegisterClassExW(&wc); HWND hwnd = ::CreateWindowW(wc.lpszClassName, L"Dear ImGui DirectX11 Example", WS_OVERLAPPEDWINDOW, 100, 100, 1280, 800, nullptr, nullptr, wc.hInstance, nullptr); // Initialize Direct3D if (!CreateDeviceD3D(hwnd)) { CleanupDeviceD3D(); ::UnregisterClassW(wc.lpszClassName, wc.hInstance); return 1; } // Show the window ::ShowWindow(hwnd, SW_SHOWDEFAULT); ::UpdateWindow(hwnd); // Setup Dear ImGui context IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); (void)io; io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable Docking io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // Enable Multi-Viewport / Platform Windows //io.ConfigViewportsNoAutoMerge = true; //io.ConfigViewportsNoTaskBarIcon = true; //io.ConfigViewportsNoDefaultParent = true; //io.ConfigDockingAlwaysTabBar = true; //io.ConfigDockingTransparentPayload = true; //io.ConfigFlags |= ImGuiConfigFlags_DpiEnableScaleFonts; // FIXME-DPI: Experimental. THIS CURRENTLY DOESN'T WORK AS EXPECTED. DON'T USE IN USER APP! //io.ConfigFlags |= ImGuiConfigFlags_DpiEnableScaleViewports; // FIXME-DPI: Experimental. // Setup Dear ImGui style ImGui::StyleColorsDark(); //ImGui::StyleColorsLight(); // When viewports are enabled we tweak WindowRounding/WindowBg so platform windows can look identical to regular ones. ImGuiStyle& style = ImGui::GetStyle(); if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { style.WindowRounding = 0.0f; style.Colors[ImGuiCol_WindowBg].w = 1.0f; } // Setup Platform/Renderer backends ImGui_ImplWin32_Init(hwnd); ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext); // Load Fonts // - If no fonts are loaded, dear imgui will use the default font. You can also load multiple fonts and use ImGui::PushFont()/PopFont() to select them. // - AddFontFromFileTTF() will return the ImFont* so you can store it if you need to select the font among multiple. // - If the file cannot be loaded, the function will return a nullptr. Please handle those errors in your application (e.g. use an assertion, or display an error and quit). // - The fonts will be rasterized at a given size (w/ oversampling) and stored into a texture when calling ImFontAtlas::Build()/GetTexDataAsXXXX(), which ImGui_ImplXXXX_NewFrame below will call. // - Use '#define IMGUI_ENABLE_FREETYPE' in your imconfig file to use Freetype for higher quality font rendering. // - Read 'docs/FONTS.md' for more instructions and details. // - Remember that in C/C++ if you want to include a backslash \ in a string literal you need to write a double backslash \\ ! //io.Fonts->AddFontDefault(); //io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeui.ttf", 18.0f); //io.Fonts->AddFontFromFileTTF("../../misc/fonts/DroidSans.ttf", 16.0f); //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Roboto-Medium.ttf", 16.0f); //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Cousine-Regular.ttf", 15.0f); //ImFont* font = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\ArialUni.ttf", 18.0f, nullptr, io.Fonts->GetGlyphRangesJapanese()); //IM_ASSERT(font != nullptr); // Our state bool show_demo_window = true; bool show_another_window = false; ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); // Main loop bool done = false; while (!done) { // Poll and handle messages (inputs, window resize, etc.) // See the WndProc() function below for our to dispatch events to the Win32 backend. MSG msg; while (::PeekMessage(&msg, nullptr, 0U, 0U, PM_REMOVE)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); if (msg.message == WM_QUIT) done = true; } if (done) break; // Handle window resize (we don't resize directly in the WM_SIZE handler) if (g_ResizeWidth != 0 && g_ResizeHeight != 0) { CleanupRenderTarget(); g_pSwapChain->ResizeBuffers(0, g_ResizeWidth, g_ResizeHeight, DXGI_FORMAT_UNKNOWN, 0); g_ResizeWidth = g_ResizeHeight = 0; CreateRenderTarget(); } // Start the Dear ImGui frame ImGui_ImplDX11_NewFrame(); ImGui_ImplWin32_NewFrame(); ImGui::NewFrame(); NewImplementationTest(); // 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!). if (show_demo_window) ImGui::ShowDemoWindow(&show_demo_window); // 2. Show a simple window that we create ourselves. We use a Begin/End pair to create a named window. { static float f = 0.0f; static int counter = 0; ImGui::Begin("Hello, world!"); // Create a window called "Hello, world!" and append into it. ImGui::Text("This is some useful text."); // Display some text (you can use a format strings too) ImGui::Checkbox("Demo Window", &show_demo_window); // Edit bools storing our window open/close state ImGui::Checkbox("Another Window", &show_another_window); ImGui::SliderFloat("float", &f, 0.0f, 1.0f); // Edit 1 float using a slider from 0.0f to 1.0f ImGui::ColorEdit3("clear color", (float*)&clear_color); // Edit 3 floats representing a color if (ImGui::Button("Button")) // Buttons return true when clicked (most widgets return true when edited/activated) counter++; ImGui::SameLine(); ImGui::Text("counter = %d", counter); ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); ImGui::End(); } // 3. Show another simple window. if (show_another_window) { ImGui::Begin("Another Window", &show_another_window); // Pass a pointer to our bool variable (the window will have a closing button that will clear the bool when clicked) ImGui::Text("Hello from another window!"); if (ImGui::Button("Close Me")) show_another_window = false; ImGui::End(); } // Rendering ImGui::Render(); const float clear_color_with_alpha[4] = { clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w }; g_pd3dDeviceContext->OMSetRenderTargets(1, &g_mainRenderTargetView, nullptr); g_pd3dDeviceContext->ClearRenderTargetView(g_mainRenderTargetView, clear_color_with_alpha); ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); // Update and Render additional Platform Windows if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { ImGui::UpdatePlatformWindows(); ImGui::RenderPlatformWindowsDefault(); } g_pSwapChain->Present(1, 0); // Present with vsync //g_pSwapChain->Present(0, 0); // Present without vsync } // Cleanup ImGui_ImplDX11_Shutdown(); ImGui_ImplWin32_Shutdown(); ImGui::DestroyContext(); CleanupDeviceD3D(); ::DestroyWindow(hwnd); ::UnregisterClassW(wc.lpszClassName, wc.hInstance); return 0; } // Helper functions bool CreateDeviceD3D(HWND hWnd) { // Setup swap chain DXGI_SWAP_CHAIN_DESC sd; ZeroMemory(&sd, sizeof(sd)); sd.BufferCount = 2; sd.BufferDesc.Width = 0; sd.BufferDesc.Height = 0; sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; sd.BufferDesc.RefreshRate.Numerator = 60; sd.BufferDesc.RefreshRate.Denominator = 1; sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; sd.OutputWindow = hWnd; sd.SampleDesc.Count = 1; sd.SampleDesc.Quality = 0; sd.Windowed = TRUE; sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; UINT createDeviceFlags = 0; //createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG; D3D_FEATURE_LEVEL featureLevel; const D3D_FEATURE_LEVEL featureLevelArray[2] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0, }; HRESULT res = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, createDeviceFlags, featureLevelArray, 2, D3D11_SDK_VERSION, &sd, &g_pSwapChain, &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext); if (res == DXGI_ERROR_UNSUPPORTED) // Try high-performance WARP software driver if hardware is not available. res = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, createDeviceFlags, featureLevelArray, 2, D3D11_SDK_VERSION, &sd, &g_pSwapChain, &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext); if (res != S_OK) return false; CreateRenderTarget(); return true; } void CleanupDeviceD3D() { CleanupRenderTarget(); if (g_pSwapChain) { g_pSwapChain->Release(); g_pSwapChain = nullptr; } if (g_pd3dDeviceContext) { g_pd3dDeviceContext->Release(); g_pd3dDeviceContext = nullptr; } if (g_pd3dDevice) { g_pd3dDevice->Release(); g_pd3dDevice = nullptr; } } void CreateRenderTarget() { ID3D11Texture2D* pBackBuffer; g_pSwapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer)); g_pd3dDevice->CreateRenderTargetView(pBackBuffer, nullptr, &g_mainRenderTargetView); pBackBuffer->Release(); } void CleanupRenderTarget() { if (g_mainRenderTargetView) { g_mainRenderTargetView->Release(); g_mainRenderTargetView = nullptr; } } #ifndef WM_DPICHANGED #define WM_DPICHANGED 0x02E0 // From Windows SDK 8.1+ headers #endif // Forward declare message handler from imgui_impl_win32.cpp extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); // Win32 message handler // You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs. // - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data. // - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data. // Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags. LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam)) return true; switch (msg) { case WM_SIZE: if (wParam == SIZE_MINIMIZED) return 0; g_ResizeWidth = (UINT)LOWORD(lParam); // Queue resize g_ResizeHeight = (UINT)HIWORD(lParam); return 0; case WM_SYSCOMMAND: if ((wParam & 0xfff0) == SC_KEYMENU) // Disable ALT application menu return 0; break; case WM_DESTROY: ::PostQuitMessage(0); return 0; case WM_DPICHANGED: if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_DpiEnableScaleViewports) { //const int dpi = HIWORD(wParam); //printf("WM_DPICHANGED to %d (%.0f%%)\n", dpi, (float)dpi / 96.0f * 100.0f); const RECT* suggested_rect = (RECT*)lParam; ::SetWindowPos(hWnd, nullptr, suggested_rect->left, suggested_rect->top, suggested_rect->right - suggested_rect->left, suggested_rect->bottom - suggested_rect->top, SWP_NOZORDER | SWP_NOACTIVATE); } break; } return ::DefWindowProcW(hWnd, msg, wParam, lParam); } ```