Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Warn users if they are reusing the same ID on a different widget #7669

Closed
pthom opened this issue Jun 7, 2024 · 10 comments
Closed

Warn users if they are reusing the same ID on a different widget #7669

pthom opened this issue Jun 7, 2024 · 10 comments
Labels
label/id and id stack implicit identifiers, pushid(), id stack

Comments

@pthom
Copy link
Contributor

pthom commented Jun 7, 2024

Version/Branch of Dear ImGui: Version 1.90.7

Bonjour Omar,

There is a common user issue which is that developers may reuse the same ID, and will not understand why their widget is not responsive.

Example

#include "hello_imgui/hello_imgui.h"
#include "imgui.h"
#include "stdio.h"

char text1[500] = "Hello,";
char text2[500] = "world!";
int value = 0;
bool flag1 = false, flag2 = false;
float f1 = 0.f, f2 = 0.f;

void Gui()
{
    // Only the first button will be clickable
    if (ImGui::Button("button")) printf("Button1 pressed\n");
    if (ImGui::Button("button")) printf("Button2 pressed\n");

    // Those two inputs will share the same value (editing one will overwrite the other)
    ImGui::InputText("text", text1, IM_ARRAYSIZE(text1));
    ImGui::InputText("text", text2, IM_ARRAYSIZE(text2));

    // The second radio button cannot be selected
    ImGui::RadioButton("radio", &value, 0);
    ImGui::RadioButton("radio", &value, 1);

    // The second checkbox cannot be toggled
    ImGui::Checkbox("checkbox", &flag1);
    ImGui::Checkbox("checkbox", &flag2);

    // Those two sliders will share the same value (editing one will overwrite the other)
    ImGui::SliderFloat("slider", &f1, 0.f, 1.f);
    ImGui::SliderFloat("slider", &f2, 0.f, 1.f);
}

int main() {    HelloImGui::Run(Gui); }

I did want to add an assert for Python users of Dear ImGui bundle, as they might not be able to understand the reason or the failure.

So I added a patch like the one you can see here:
pthom@ea95177

With this patch, the example code would throw an assert for each reused ID.


The patch adds this method:

// a version of GetID that warns if the ID was already used
IMGUI_API ImGuiID ImGuiWindow::GetID_AssertUnique(const char* str_id);

The principle is to use this method in some rare and carefuly selected locations (ButtonEx, ArrowButtonEx, Checkbox, RadioButton , DragScalar, SliderScalar, Drag, InputTextEx), i.e. the most commonly used widgets, and to warn the user if they are using an existing ID.

The implementation is simple: just remember the already pushed IDs, and raise an assert if the user is raising the same ID in the same frame. This concerns only a very limited set of the IDs (i.e. those for the aforementioned widgets).

Would you be interested in such a change? If so, I can push a PR. I know that my current implementation can be improved and should not use a static variable (instead, perhaps rely on a member of ImGuiWindow)

I know that this change could have some implications in a lot of user code. But I would think that it is for the best because they would be warned that they were doing something bad.

As an encouraging hint, I tested it successfully in all of the numerous demonstrations of Dear ImGui Bundle, and it worked correctly

Feel free to close if you do not feel that this is useful.

Note: this feature could be protected by a compilation flag, in order to avoid potential regressions in user code.

Cheers!

@pthom
Copy link
Contributor Author

pthom commented Jun 7, 2024

Side note: I did add a related patch for the StackLayout feature (ImGui::Begin_Horizontal + ImGui::Begin_Vertical), because reusing the same ID would lead to an infinite loop (and Python users would not be able to guess the reason for this infinite loop): see pthom@8b45d6b

@ocornut ocornut added the label/id and id stack implicit identifiers, pushid(), id stack label Jun 7, 2024
@ocornut
Copy link
Owner

ocornut commented Jun 7, 2024

This implementation is unfortunately extremely CPU costly, to a point where it is going to be unacceptable for performance.
Even if you used a better data structure for it, it would likely be too much. On a complex UI this is likely to cost more than the entirety of the rest of Dear ImGui. We quite rarely perform non-O(1) operations on standard widgets.

My strategy for this would be to perform a compare based on a shared field that's normally set once a frame, e.g. g.HoveredId, so it would become a lightweight and O(1) check, with the limitation that it would trigger during interaction (e.g. hover), but considering the alternative is redhibitory it seems like a good compromise.

I don't necessary agree that an assert is ideal, but we would turn that detection into an overlay warning with a button to break into either items. We would need to use there is a way (e.g. via ItemFlags) to allow it as I am convinced there are situation where it would be useful.

@pthom
Copy link
Contributor Author

pthom commented Jun 7, 2024

Hello again Omar,

Many thanks for your quick answer!

This answer is only related to the CPU cost part : the question whether this is a good idea, or whether this is a good way to implement this is still opened.

I did measure the CPU usage of my implementation.

For this, I used a simple benchmark utility, see this commit:
pthom@839d331

And I tested this with quite a lot of widgets. See video below:

bench.mp4

And the results are (for 1 minutes usage of an application with lots of varied widgets)

Function              |Nb calls|Total time|Av. time|Deviation|
----------------------+--------+----------+--------+---------+
GetId(const char*)    |  821501| 126.161ms| 0.154us|  2.275us|
GetID_AssertUniquePart|  260980|  19.860ms| 0.076us|  0.207us|
GetId(int)            |   91992|   3.090ms| 0.034us|  0.097us|
GetId(void*)          |   10523|   0.629ms| 0.060us|  0.091us|

PS: I wrote this benchmark utility in a previous life. It is quite easy to setup, which is why I chose it. I suspect another benchmarking tool would lead to similar results.

@GamingMinds-DanielC
Copy link
Contributor

And I tested this with quite a lot of widgets. See video below:

Looks like quite a few widgets, but separated into different tabs and not that many widgets each frame. Your implementation searches linearly through a vector for each id it has to check while the same vector grows linearly as well. That's quadratic runtime and can escalate pretty quickly.

If the deviation in your benchmark table is what I think it is (difference between minimum and maximum time per call), then GetId(const char*) already grows to take almost 15x as long as the average call. And your average call already includes the id check, so even worse compared to the unmodified version.

@ocornut
Copy link
Owner

ocornut commented Jun 7, 2024

It’s indeed a small amount of widgets. Calling 1-10k/frame would be a good base but i don’t need to see benchmarks to know it’s going to be too expensive. The entire codebase is designed to avoid this sorts of scan as much as possible.

@pthom
Copy link
Contributor Author

pthom commented Jun 7, 2024

It’s indeed a small amount of widgets. Calling 1-10k/frame would be a good base but i don’t need to see benchmarks to know it’s going to be too expensive. The entire codebase is designed to avoid this sorts of scan as much as possible.

I see your point. I didn't mean to come across as overconfident, and I apologize if I did.

This issue was indeed just a suggestion, and potential food for thought for a possible implementation. If this issue is deemed useful, a totally different implementation could be used, of course.

Regarding the cache, it's quite small and could be limited to a max size (e.g., 100 entries, around 400 bytes). This would fit easily in the L1 cache, making a linear scan relatively fast. This revised implementation is an attempt in this direction.

@ocornut
Copy link
Owner

ocornut commented Jun 8, 2024

I provided a solution that would be near free and react on hover, it seems like a good enough tradeoff ?

@pthom
Copy link
Contributor Author

pthom commented Jun 11, 2024

My strategy for this would be to perform a compare based on a shared field that's normally set once a frame, e.g. g.HoveredId, so it would become a lightweight and O(1) check, with the limitation that it would trigger during interaction (e.g. hover). we would turn that detection into an overlay warning with a button to break into either items. We would need to use there is a way (e.g. via ItemFlags) to allow it as I am convinced there are situation where it would be useful.

I'm not sure I understood how to implement your proposed solution.

perform a compare based on a shared field that's normally set once a frame,

This is the part I do not understand

we would turn that detection into an overlay warning with a button to break into either items

IMHO we would need to keep track of the already registered IDs somewhere. Am I wrong here? In that case, we could limit the loop to only when the item is hovered.

But actually I think you had another idea which I did not understand.

Anyhow, there is no emergency.

Cheers

PS: I'm quite busy on another project at the moment. I might contact you by email in the next days to tell you about it. I would like your feedback (if you have time).

pthom added a commit to pthom/imgui_bundle that referenced this issue Jun 12, 2024
@pthom
Copy link
Contributor Author

pthom commented Jul 5, 2024

Additional info (if using the current version of the patch): there is an additional related commit that is needed
pthom@97622b0

After running for quite a while with this patch, the only places that do reuse the Id are TempInputScalar and TempInputText. This additional commit handles this.

pthom added a commit to pthom/imgui that referenced this issue Sep 6, 2024
…cornut#7669)

(fix for ocornut#7669)

There is a common user issue which is that developers may reuse the same ID, and will not understand why their widget is not responsive.

This PR will:
* detect when the same ID is used on different widgets (only if the widget is hovered, to avoid a O(n^2) search at each frame)
* display a warning tooltip to the user, with hints on how to fix the issue (e.g. use PushID, or add a ## to the label)
* allow the user to break in the debugger on any of the widgets that use the same ID

**View from 10,000 feet**
See this video for a quick overview of the feature and how it is implemented: https://traineq.org/ImGui/ImGui_PR_Warn_reuse_ID_Explanation.mp4

# Summary of changes

## struct ImGuiContext: added fields UidIXXX (UidsThisFrame & co)

```cpp
struct ImGuiContext
{
    ...
    // Declaration
    ImVector<ImGuiID>       UidsThisFrame;                      // A list of item IDs submitted this frame (used to detect and warn about duplicate ID usage).
    int                     UidAllowDuplicatesStack;            // Stack depth for allowing duplicate IDs (if > 0, duplicate IDs are allowed). See PushAllowDuplicateID / PopAllowDuplicateID
    ImGuiID                 UidHighlightedDuplicate;            // Will be set if a duplicated item is hovered (all duplicated items will appear with a red dot in the top left corner on the next frame)
    int                     UidHighlightedTimestamp;            // Timestamp (FrameCount) of the highlight (which will be shown on the next frame)
    bool                    UidWasTipDisplayed;                 // Will be set to true to avoid displaying multiple times the same tooltip
    ...

    // Constructor
    ImGuiContext(ImFontAtlas* shared_font_atlas) {
        ...
        UidsThisFrame.clear();
        UidAllowDuplicatesStack = 0;
        UidHighlightedDuplicate = 0;
        UidWasTipDisplayed = false;
        ...
    }
};
```

## struct ImGuiWindow: added ReserveUniqueID(), beside GetID()

imgui_internal.h:2572 / near GetID() existing methods
```cpp
struct ImGuiWindow {
    ...
    // ReserveUniqueID: returns GetID(), but may display a warning. Should be called only once per frame with a given ID
    ImGuiID     ReserveUniqueID(const char* str_id);
    ...
};
```

## Added PushAllowDuplicateID / PopAllowDuplicateID

imgui_internal.h:3057  / near related ID functions (SetActiveID, GetIDWithSeed, ...)
```cpp
    ...
    IMGUI_API void          PushAllowDuplicateID();         // Disables the tooltip warning when duplicate IDs are detected.
    IMGUI_API void          PopAllowDuplicateID();          // Re-enables the tooltip warning when duplicate IDs are detected.
    ...
```

imgui.cpp:3873 / near ReserveUniqueID() implementation
```cpp
IMGUI_API void ImGui::PushAllowDuplicateID()
{
    ImGuiContext& g = *GImGui;
    g.UidAllowDuplicatesStack++;
}

IMGUI_API void ImGui::PopAllowDuplicateID()
{
    ImGuiContext& g = *GImGui;
    IM_ASSERT(g.UidAllowDuplicatesStack > 0 && "Mismatched PopAllowDuplicateID()");
    g.UidAllowDuplicatesStack--;
}
```

## Main logic: ImGui::EndFrame(), NewFrame() and ImGuiWindow::ReserveUniqueID()

### ImGui::NewFrame():
```cpp
    ...
    g.UidsThisFrame.resize(0);  // Empty the list of items, but keep its allocated data
    g.UidWasTipDisplayed = false;
```

### ImGui::EndFrame():
```cpp
    IM_ASSERT(g.UidAllowDuplicatesStack == 0 && "Mismatched Push/PopAllowDuplicateID");
    if (g.UidHighlightedTimestamp != ImGui::GetFrameCount())
    {
        g.UidHighlightedTimestamp = 0;
        g.UidHighlightedDuplicate = 0;
    }
```

### ImGuiWindow::ReserveUniqueID() / imgui.cpp, at the end of "[SECTION] ID STACK"
The pseudocode below summarizes its behavior:
```cpp
IMGUI_API ImGuiID ImGuiWindow::ReserveUniqueID(const char* str_id)
{
    ImGuiContext& g = *GImGui;
    ImGuiID id = GetID(str_id);

    // If PushAllowDuplicateID was called, do not check for uniqueness
    if (g.UidAllowDuplicatesStack != 0)
    return id;

    // Visual aid: a red circle in the top-left corner of all widgets that use a duplicate of the id
    if (id == g.UidHighlightedDuplicate)
        Draw Red dot on top left corner

    // Only check for uniqueness if hovered (this avoid a O(n^2) search at each frame)
    if (id == ImGui::GetHoveredID())
    {
        if (g.UidsThisFrame.contains(id) && !g.UidWasTipDisplayed)
        {
            // Prepare highlight on the next frame for widgets that use this ID
            g.UidHighlightedDuplicate = id;
            g.UidHighlightedTimestamp = ImGui::GetFrameCount();
            if (ImGui::BeginTooltip())
            {
                if (! g.DebugItemPickerActive)
                {
                    // Display tooltip showing the duplicate Id as a string
                    ...
                    // Show hints for correction (PushID, ##)
                    ...

                    ImGui::Text("    Press Ctrl-P to break in any of those items    ");
                    if Ctrl-P
                        g.DebugItemPickerActive = true;
                }
                else
                {
                    // Add additional info at the bottom of the ItemPicker tooltip
                    ImGui::Text("Click on one of the widgets which uses a duplicated item");
                }
                ImGui::EndTooltip();
            }
            g.UidWasTipDisplayed = true;
        }
    }
    g.UidsThisFrame.push_back(id);
    return id;
}
```

## imgui_widgets.cpp: use ReserveUniqueID, allow duplicates when needed (TempInputText, BeginMenuEx):

I went through all the usages of window->GetID() and changed them to ReserveUniqueID when it was possible / desirable.

### Places with one-line changes

In all the places below, the change was extremely simple: I simply replaced `window->GetID` by `window->ReserveUniqueID`.

For example, in ImGui::ButtonEx
```cpp
bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
    ...
    const ImGuiID id = window->ReserveUniqueID(label);  // Instead of window->GetID
```

Below is the list of all function where this simple change was applied:
```cpp
    InvisibleButton
    ArrowButtonEx
    GetWindowScrollbarID
    ImageButton  (both versions)
    Checkbox
    RadioButton
    BeginCombo
    DragScalar
    SliderScalar
    VSliderScalar
    InputTextEx
    ColorButton
    CollapsingHeader
    Selectable
    PlotEx
    BeginTabBar
    TabBarCalcTabID
    TreeNode(const char *)
    TreeNodeEx(const char *)
    TreeNodeExV(const char *)
```

### Places where duplicates are needed

#### TempInputText

TempInputText create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets)
It reuses the widget ID, so we allow it temporarily via PushAllowDuplicateID / PopAllowDuplicateID
```
bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
{
    ImGui::PushAllowDuplicateID();
    ...
    ImGui::PopAllowDuplicateID();
}

```

#### GetWindowScrollbarID
GetWindowScrollbarID had to be split in two: ReserveWindowScrollbarID will perform the ID reservation, while GetWindowScrollbarID is only a query
Luckily, this is the only instance where this was required.

#### BeginMenuEx

In BeginMenuEx, we initially call GetID() instead of ReserveUniqueID() because were are not "consuming" this ID:
instead, we are only querying IsPopupOpen().
The ID will be consumed later by calling ImGui::PushID(label) + Selectable("") (which will lead to the same ID)

```
bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
{
    const ImGuiID id = window->GetID(label);
    bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None);

    ...

    PushID(label);
    pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, label_size.y));

    ...
}
```

#### Places with no changes, but worth mentioning

* TreeNode(const void *): unchanged
`TreeNode(const void* ptr_id)` and `TreeNodeExV(const void *)` are the only instances which call `window->GetID(const void *)`.
As a consequence, they cannot use the unique id mechanism, but I think their usage is rare enough that is not a big concern.

* ImGui::TabBarCalcTabID: unchanged
It still uses window->GetID, to avoid issues when merging with the docking branch.

* BeginChild, BeginPopup and co are unchanged:
  since they use ImGui::Begin()/End() internally), I suspect they might be called multiple times with the same string,
  since it is allowed with ImGui::Begin/End.

## imgui_demo.cpp: some minor changes

I went through the full demo Window to look for instances where a duplicate ID might be used, and corrected them.
Very few changes were required:

* "Layout/Text Baseline Alignment": simple ID fix  (ImGui::Button("Some framed item#ocornut#2"))
* Widgets/Plotting: simple id fix (ImGui::PlotLines("Lines#ocornut#2", ...), ImGui::PlotHistogram("Histogram#ocornut#2", ...))
* Columns (legacy API)/Borders: added ImGui::PushID for each cell
* Widgets/Drag and Drop/Drag to reorder items (simple): no change, but a transient duplicate ID may appear during reordering (for *one* frame only).

# Demo code in order to test this PR

Add and call this function in any imgui example. This code will trigger the warnings for duplicated IDs,
and will also enable to make sure that the fixes that had to be done are OK.

```cpp
char text1[500] = "Hello,";
char text2[500] = "world!";
char text3[500] = "Hello,";
char text4[500] = "world!";
int value = 0;
bool flag1 = false, flag2 = false;
float f1 = 0.f, f2 = 0.f, f3 = 0.f, f4 = 0.f;

void Gui()
{
    // Some widgets to show the duplicated ID warnings
    ImGui::SetNextWindowSize(ImVec2(400, 500));
    if (ImGui::Begin("Duplicated ID examples", nullptr, ImGuiWindowFlags_MenuBar))
    {
        ImGui::TextWrapped("Example of widgets using duplicated IDs");
        ImGui::Separator();

        ImGui::TextWrapped("Only the first button is clickable (same ID)");
        if (ImGui::Button("button")) printf("Button1 pressed\n");
        if (ImGui::Button("button")) printf("Button2 pressed\n");

        ImGui::Separator();

        ImGui::TextWrapped("Those text inputs will share the same value (editing one will overwrite the other)");
        ImGui::InputText("text", text1, IM_ARRAYSIZE(text1));
        ImGui::InputText("text", text2, IM_ARRAYSIZE(text2));

        ImGui::Separator();

        ImGui::TextWrapped("Those radio buttons will share the same value (selecting one will overwrite the other)");
        ImGui::RadioButton("radio", &value, 0);
        ImGui::RadioButton("radio", &value, 1);

        ImGui::Separator();

        ImGui::TextWrapped("The second checkbox cannot be toggled");
        ImGui::Checkbox("checkbox", &flag1);
        ImGui::Checkbox("checkbox", &flag2);

        ImGui::Separator();

        ImGui::TextWrapped("Those two sliders will share the same value (editing one will overwrite the other");
        ImGui::SliderFloat("slider", &f1, 0.f, 1.f);
        ImGui::SliderFloat("slider", &f2, 0.f, 1.f);

        ImGui::Separator();
    }
    ImGui::End();

    // Some widgets to test the fixes that had to be done
    ImGui::SetNextWindowSize(ImVec2(400, 400));
    if (ImGui::Begin("Regression tests", nullptr, ImGuiWindowFlags_MenuBar))
    {
        ImGui::TextWrapped("Small issues had to be fixed inside DragFloat (cf TempInputText), InputTextMultiline and BeginMenuBar.\n"
            "This window is here to test those fixes.");

        ImGui::Separator();

        if (ImGui::BeginMenuBar())
        {
            if (ImGui::BeginMenu("My Menu"))
            {
                ImGui::MenuItem("Item 1");
                ImGui::EndMenu();
            }
            ImGui::EndMenuBar();
        }

        ImGui::TextWrapped("DragFloat will reuse the Item ID when double clicking. This is allowed via Push/PopAllowDuplicateID");
        ImGui::DragFloat("dragf", &f3);

        ImGui::Separator();

        ImGui::TextWrapped("An issue was fixed with the InputTextMultiline vertical scrollbar, which trigerred a duplicate ID warning, which was fixed");
        ImGui::InputTextMultiline("multiline", text1, 500);

    }
    ImGui::End();

    // A window that is populated in several steps should still detect duplicates
    {
        ImGui::SetNextWindowSize(ImVec2(400, 200));

        ImGui::Begin("Window populated in several steps");
        ImGui::TextWrapped("This window is populated between several ImGui::Begin/ImGui::End calls (with the same window name).\n"
                           "Duplicates should be detected anyhow.");
        ImGui::Text("A first InputText");
        ImGui::InputText("TTT", text3, 500);
        ImGui::End();

        ImGui::Begin("Window populated in several steps");
        ImGui::Separator();
        ImGui::Text("The second InputText below reuses the same ID, \nin a different ImGui::Begin/ImGui::End context \n(but with the same window name)");
        ImGui::InputText("TTT", text4, 500);
        ImGui::End();
    }
}
```
pthom added a commit to pthom/imgui that referenced this issue Sep 6, 2024
(fix for ocornut#7669)

There is a common user issue which is that developers may reuse the same ID, and will not understand why their widget is not responsive.

This PR will:
* detect when the same ID is used on different widgets (only if the widget is hovered, to avoid a O(n^2) search at each frame)
* display a warning tooltip to the user, with hints on how to fix the issue (e.g. use PushID, or add a ## to the label)
* allow the user to break in the debugger on any of the widgets that use the same ID
* enable to disable the warning when needed (e.g. when reusing the ID is intentional, via PushAllowDuplicateID / PopAllowDuplicateID)
* enable to debug all duplicates at once (by defining IMGUI_DEBUG_PARANOID)

**View from 10,000 feet**

This PR adds a distinction between

```cpp
    // Same as before (computes the hash of the string)
    ImGuiID     GetID(const char* str, const char* str_end = NULL);
 ```
and
```cpp

    ImGuiID     ReserveUniqueID(const char* str_id);  // returns GetID(), but may display a warning tooltip if the ID is not unique. Should be called only once per frame with a given ID stack.
```

This distinction was applied to several widgets inside imgui_widgets.cpp which now call ReserveUniqueID instead of GetID,
when their intention is to reserve a unique ID for the widget.

See this video for a quick overview of the feature and how it is implemented: https://traineq.org/ImGui/ImGui_PR_Warn_reuse_ID_Explanation.mp4

# Summary of changes

## struct ImGuiContext: added fields UidIXXX (UidsThisFrame & co)

```cpp
struct ImGuiContext
{
    ...
    // Declaration
    ImVector<ImGuiID>       UidsThisFrame;                      // A list of item IDs submitted this frame (used to detect and warn about duplicate ID usage).
    int                     UidAllowDuplicatesStack;            // Stack depth for allowing duplicate IDs (if > 0, duplicate IDs are allowed). See PushAllowDuplicateID / PopAllowDuplicateID
    ImGuiID                 UidHighlightedDuplicate;            // Will be set if a duplicated item is hovered (all duplicated items will appear with a red dot in the top left corner on the next frame)
    int                     UidHighlightedTimestamp;            // Timestamp (FrameCount) of the highlight (which will be shown on the next frame)
    bool                    UidWasTipDisplayed;                 // Will be set to true to avoid displaying multiple times the same tooltip
    ...

    // Constructor
    ImGuiContext(ImFontAtlas* shared_font_atlas) {
        ...
        UidsThisFrame.clear();
        UidAllowDuplicatesStack = 0;
        UidHighlightedDuplicate = 0;
        UidWasTipDisplayed = false;
        ...
    }
};
```

## struct ImGuiWindow: added ReserveUniqueID(), beside GetID()

imgui_internal.h:2572 / near GetID() existing methods
```cpp
struct ImGuiWindow {
    ...
    // ReserveUniqueID: returns GetID(), but may display a warning. Should be called only once per frame with a given ID
    ImGuiID     ReserveUniqueID(const char* str_id);
    ...
};
```

## Added PushAllowDuplicateID / PopAllowDuplicateID

imgui_internal.h:3057  / near related ID functions (SetActiveID, GetIDWithSeed, ...)
```cpp
    ...
    IMGUI_API void          PushAllowDuplicateID();         // Disables the tooltip warning when duplicate IDs are detected.
    IMGUI_API void          PopAllowDuplicateID();          // Re-enables the tooltip warning when duplicate IDs are detected.
    ...
```

imgui.cpp:3873 / near ReserveUniqueID() implementation
```cpp
IMGUI_API void ImGui::PushAllowDuplicateID()
{
    ImGuiContext& g = *GImGui;
    g.UidAllowDuplicatesStack++;
}

IMGUI_API void ImGui::PopAllowDuplicateID()
{
    ImGuiContext& g = *GImGui;
    IM_ASSERT(g.UidAllowDuplicatesStack > 0 && "Mismatched PopAllowDuplicateID()");
    g.UidAllowDuplicatesStack--;
}
```

## Main logic: ImGui::EndFrame(), NewFrame() and ImGuiWindow::ReserveUniqueID()

### ImGui::NewFrame():
```cpp
    ...
    g.UidsThisFrame.resize(0);  // Empty the list of items, but keep its allocated data
    g.UidWasTipDisplayed = false;
```

### ImGui::EndFrame():
```cpp
    IM_ASSERT(g.UidAllowDuplicatesStack == 0 && "Mismatched Push/PopAllowDuplicateID");
    if (g.UidHighlightedTimestamp != ImGui::GetFrameCount())
    {
        g.UidHighlightedTimestamp = 0;
        g.UidHighlightedDuplicate = 0;
    }
```

### ImGuiWindow::ReserveUniqueID() / imgui.cpp, at the end of "[SECTION] ID STACK"
The pseudocode below summarizes its behavior:
```cpp
IMGUI_API ImGuiID ImGuiWindow::ReserveUniqueID(const char* str_id)
{
    ImGuiContext& g = *GImGui;
    ImGuiID id = GetID(str_id);

    // If PushAllowDuplicateID was called, do not check for uniqueness
    if (g.UidAllowDuplicatesStack != 0)
    return id;

    // Visual aid: a red circle in the top-left corner of all widgets that use a duplicate of the id
    if (id == g.UidHighlightedDuplicate)
        Draw Red dot on top left corner

    // Only check for uniqueness if hovered (this avoid a O(n^2) search at each frame)
    if (id == ImGui::GetHoveredID())
    {
        if (g.UidsThisFrame.contains(id) && !g.UidWasTipDisplayed)
        {
            // Prepare highlight on the next frame for widgets that use this ID
            g.UidHighlightedDuplicate = id;
            g.UidHighlightedTimestamp = ImGui::GetFrameCount();
            if (ImGui::BeginTooltip())
            {
                if (! g.DebugItemPickerActive)
                {
                    // Display tooltip showing the duplicate Id as a string
                    ...
                    // Show hints for correction (PushID, ##)
                    ...

                    ImGui::Text("    Press Ctrl-P to break in any of those items    ");
                    if Ctrl-P
                        g.DebugItemPickerActive = true;
                }
                else
                {
                    // Add additional info at the bottom of the ItemPicker tooltip
                    ImGui::Text("Click on one of the widgets which uses a duplicated item");
                }
                ImGui::EndTooltip();
            }
            g.UidWasTipDisplayed = true;
        }
    }
    g.UidsThisFrame.push_back(id);
    return id;
}
```

## imgui_widgets.cpp: use ReserveUniqueID, allow duplicates when needed (TempInputText, BeginMenuEx):

I went through all the usages of window->GetID() and changed them to ReserveUniqueID when it was possible / desirable.

### Places with one-line changes

In all the places below, the change was extremely simple: I simply replaced `window->GetID` by `window->ReserveUniqueID`.

For example, in ImGui::ButtonEx
```cpp
bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
    ...
    const ImGuiID id = window->ReserveUniqueID(label);  // Instead of window->GetID
```

Below is the list of all function where this simple change was applied:
```cpp
    InvisibleButton
    ArrowButtonEx
    GetWindowScrollbarID
    ImageButton  (both versions)
    Checkbox
    RadioButton
    BeginCombo
    DragScalar
    SliderScalar
    VSliderScalar
    InputTextEx
    ColorButton
    CollapsingHeader
    Selectable
    PlotEx
    BeginTabBar
    TabBarCalcTabID
    TreeNode(const char *)
    TreeNodeEx(const char *)
    TreeNodeExV(const char *)
```

### Places where duplicates are needed

#### TempInputText

TempInputText create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets)
It reuses the widget ID, so we allow it temporarily via PushAllowDuplicateID / PopAllowDuplicateID
```
bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
{
    ImGui::PushAllowDuplicateID();
    ...
    ImGui::PopAllowDuplicateID();
}

```

#### GetWindowScrollbarID
GetWindowScrollbarID had to be split in two: ReserveWindowScrollbarID will perform the ID reservation, while GetWindowScrollbarID is only a query
Luckily, this is the only instance where this was required.

#### BeginMenuEx

In BeginMenuEx, we initially call GetID() instead of ReserveUniqueID() because were are not "consuming" this ID:
instead, we are only querying IsPopupOpen().
The ID will be consumed later by calling ImGui::PushID(label) + Selectable("") (which will lead to the same ID)

```
bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
{
    const ImGuiID id = window->GetID(label);
    bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None);

    ...

    PushID(label);
    pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, label_size.y));

    ...
}
```

#### Places with no changes, but worth mentioning

* TreeNode(const void *): unchanged
`TreeNode(const void* ptr_id)` and `TreeNodeExV(const void *)` are the only instances which call `window->GetID(const void *)`.
As a consequence, they cannot use the unique id mechanism, but I think their usage is rare enough that is not a big concern.

* ImGui::TabBarCalcTabID: unchanged
It still uses window->GetID, to avoid issues when merging with the docking branch.

* BeginChild, BeginPopup and co are unchanged:
  since they use ImGui::Begin()/End() internally), I suspect they might be called multiple times with the same string,
  since it is allowed with ImGui::Begin/End.

## imgui_demo.cpp: some minor changes

I went through the full demo Window to look for instances where a duplicate ID might be used, and corrected them.
Very few changes were required:

* "Layout/Text Baseline Alignment": simple ID fix  (ImGui::Button("Some framed item#ocornut#2"))
* Widgets/Plotting: simple id fix (ImGui::PlotLines("Lines#ocornut#2", ...), ImGui::PlotHistogram("Histogram#ocornut#2", ...))
* Columns (legacy API)/Borders: added ImGui::PushID for each cell
* Widgets/Drag and Drop/Drag to reorder items (simple): no change, but a transient duplicate ID may appear during reordering (for *one* frame only).

# Demo code in order to test this PR

Add and call this function in any imgui example. This code will trigger the warnings for duplicated IDs,
and will also enable to make sure that the fixes that had to be done are OK.

```cpp
char text1[500] = "Hello,";
char text2[500] = "world!";
char text3[500] = "Hello,";
char text4[500] = "world!";
int value = 0;
bool flag1 = false, flag2 = false;
float f1 = 0.f, f2 = 0.f, f3 = 0.f, f4 = 0.f;

void Gui()
{
    // Some widgets to show the duplicated ID warnings
    ImGui::SetNextWindowSize(ImVec2(400, 500));
    if (ImGui::Begin("Duplicated ID examples", nullptr, ImGuiWindowFlags_MenuBar))
    {
        ImGui::TextWrapped("Example of widgets using duplicated IDs");
        ImGui::Separator();

        ImGui::TextWrapped("Only the first button is clickable (same ID)");
        if (ImGui::Button("button")) printf("Button1 pressed\n");
        if (ImGui::Button("button")) printf("Button2 pressed\n");

        ImGui::Separator();

        ImGui::TextWrapped("Those text inputs will share the same value (editing one will overwrite the other)");
        ImGui::InputText("text", text1, IM_ARRAYSIZE(text1));
        ImGui::InputText("text", text2, IM_ARRAYSIZE(text2));

        ImGui::Separator();

        ImGui::TextWrapped("Those radio buttons will share the same value (selecting one will overwrite the other)");
        ImGui::RadioButton("radio", &value, 0);
        ImGui::RadioButton("radio", &value, 1);

        ImGui::Separator();

        ImGui::TextWrapped("The second checkbox cannot be toggled");
        ImGui::Checkbox("checkbox", &flag1);
        ImGui::Checkbox("checkbox", &flag2);

        ImGui::Separator();

        ImGui::TextWrapped("Those two sliders will share the same value (editing one will overwrite the other");
        ImGui::SliderFloat("slider", &f1, 0.f, 1.f);
        ImGui::SliderFloat("slider", &f2, 0.f, 1.f);

        ImGui::Separator();
    }
    ImGui::End();

    // Some widgets to test the fixes that had to be done
    ImGui::SetNextWindowSize(ImVec2(400, 400));
    if (ImGui::Begin("Regression tests", nullptr, ImGuiWindowFlags_MenuBar))
    {
        ImGui::TextWrapped("Small issues had to be fixed inside DragFloat (cf TempInputText), InputTextMultiline and BeginMenuBar.\n"
            "This window is here to test those fixes.");

        ImGui::Separator();

        if (ImGui::BeginMenuBar())
        {
            if (ImGui::BeginMenu("My Menu"))
            {
                ImGui::MenuItem("Item 1");
                ImGui::EndMenu();
            }
            ImGui::EndMenuBar();
        }

        ImGui::TextWrapped("DragFloat will reuse the Item ID when double clicking. This is allowed via Push/PopAllowDuplicateID");
        ImGui::DragFloat("dragf", &f3);

        ImGui::Separator();

        ImGui::TextWrapped("An issue was fixed with the InputTextMultiline vertical scrollbar, which trigerred a duplicate ID warning, which was fixed");
        ImGui::InputTextMultiline("multiline", text1, 500);

    }
    ImGui::End();

    // A window that is populated in several steps should still detect duplicates
    {
        ImGui::SetNextWindowSize(ImVec2(400, 200));

        ImGui::Begin("Window populated in several steps");
        ImGui::TextWrapped("This window is populated between several ImGui::Begin/ImGui::End calls (with the same window name).\n"
                           "Duplicates should be detected anyhow.");
        ImGui::Text("A first InputText");
        ImGui::InputText("TTT", text3, 500);
        ImGui::End();

        ImGui::Begin("Window populated in several steps");
        ImGui::Separator();
        ImGui::Text("The second InputText below reuses the same ID, \nin a different ImGui::Begin/ImGui::End context \n(but with the same window name)");
        ImGui::InputText("TTT", text4, 500);
        ImGui::End();
    }
}
```
pthom added a commit to pthom/imgui that referenced this issue Sep 6, 2024
(fix for ocornut#7669)

There is a common user issue which is that developers may reuse the same ID, and will not understand why their widget is not responsive.

This PR will:
* detect when the same ID is used on different widgets (only if the widget is hovered, to avoid a O(n^2) search at each frame)
* display a warning tooltip to the user, with hints on how to fix the issue (e.g. use PushID, or add a ## to the label)
* allow the user to break in the debugger on any of the widgets that use the same ID
* enable to disable the warning when needed (e.g. when reusing the ID is intentional, via PushAllowDuplicateID / PopAllowDuplicateID)
* enable to debug all duplicates at once (by defining IMGUI_DEBUG_PARANOID)

**View from 10,000 feet**

This PR adds a distinction between

```cpp
// Same as before
ImGuiID     GetID(const char* str, const char* str_end = NULL);
 ```
and
```cpp
// returns GetID(), but may display a warning tooltip if the ID is not unique. Should be called only once per frame with a given ID stack.
ImGuiID     ReserveUniqueID(const char* str_id);
```

This distinction was applied to several widgets inside imgui_widgets.cpp which now call ReserveUniqueID instead of GetID,
when their intention is to reserve a unique ID for the widget.

See this video for a quick overview of the feature and how it is implemented:

https://traineq.org/ImGui/ImGui_PR_Warn_reuse_ID_Explanation.mp4

> _Note: if desired, a compile flag (e.g. IMGUI_NO_WARN_DUPLICATE_ID or its inverse) could be added to disable the warning and make ReserveUniqueID behave like GetID()_

# Summary of changes

## struct ImGuiContext: added fields UidIXXX (UidsThisFrame & co)

```cpp
struct ImGuiContext
{
    ...
    // Declaration
    ImVector<ImGuiID>       UidsThisFrame;                      // A list of item IDs submitted this frame (used to detect and warn about duplicate ID usage).
    int                     UidAllowDuplicatesStack;            // Stack depth for allowing duplicate IDs (if > 0, duplicate IDs are allowed). See PushAllowDuplicateID / PopAllowDuplicateID
    ImGuiID                 UidHighlightedDuplicate;            // Will be set if a duplicated item is hovered (all duplicated items will appear with a red dot in the top left corner on the next frame)
    int                     UidHighlightedTimestamp;            // Timestamp (FrameCount) of the highlight (which will be shown on the next frame)
    bool                    UidWasTipDisplayed;                 // Will be set to true to avoid displaying multiple times the same tooltip
    ...

    // Constructor
    ImGuiContext(ImFontAtlas* shared_font_atlas) {
        ...
        UidsThisFrame.clear();
        UidAllowDuplicatesStack = 0;
        UidHighlightedDuplicate = 0;
        UidHighlightedTimestamp = 0;
        UidWasTipDisplayed = false;
        ...
    }
};
```

## struct ImGuiWindow: added ReserveUniqueID(), beside GetID()

imgui_internal.h / near GetID() existing methods
```cpp
struct ImGuiWindow {
    ...
    // ReserveUniqueID: returns GetID(), but may display a warning. Should be called only once per frame with a given ID
    ImGuiID     ReserveUniqueID(const char* str_id);
    ...
};
```

## Added PushAllowDuplicateID / PopAllowDuplicateID

imgui_internal.h  / near related ID functions (SetActiveID, GetIDWithSeed, ...)
```cpp
    ...
    IMGUI_API void          PushAllowDuplicateID();         // Disables the tooltip warning when duplicate IDs are detected.
    IMGUI_API void          PopAllowDuplicateID();          // Re-enables the tooltip warning when duplicate IDs are detected.
    ...
```

imgui.cpp:3873 / near ReserveUniqueID() implementation
```cpp
IMGUI_API void ImGui::PushAllowDuplicateID()
{
    ImGuiContext& g = *GImGui;
    g.UidAllowDuplicatesStack++;
}

IMGUI_API void ImGui::PopAllowDuplicateID()
{
    ImGuiContext& g = *GImGui;
    IM_ASSERT(g.UidAllowDuplicatesStack > 0 && "Mismatched PopAllowDuplicateID()");
    g.UidAllowDuplicatesStack--;
}
```

> _Note: doing this via ItemFlags was not feasible as many items (e.g. Button, Checkbox) do not expose flags_

## Main logic: ImGui::EndFrame(), NewFrame() and ImGuiWindow::ReserveUniqueID()

### ImGui::NewFrame():
```cpp
    ...
    g.UidsThisFrame.resize(0);  // Empty the list of items, but keep its allocated data
    g.UidWasTipDisplayed = false;
```

### ImGui::EndFrame():
```cpp
    IM_ASSERT(g.UidAllowDuplicatesStack == 0 && "Mismatched Push/PopAllowDuplicateID");
    if (g.UidHighlightedTimestamp != ImGui::GetFrameCount())
    {
        g.UidHighlightedTimestamp = 0;
        g.UidHighlightedDuplicate = 0;
    }
```

### ImGuiWindow::ReserveUniqueID()
in imgui.cpp, at the end of "[SECTION] ID STACK"

The pseudocode below summarizes its behavior:
```cpp
IMGUI_API ImGuiID ImGuiWindow::ReserveUniqueID(const char* str_id)
{
    ImGuiContext& g = *GImGui;
    ImGuiID id = GetID(str_id);

    // If PushAllowDuplicateID was called, do not check for uniqueness
    if (g.UidAllowDuplicatesStack != 0)
    return id;

    // Visual aid: a red circle in the top-left corner of all widgets that use a duplicate of the id
    if (id == g.UidHighlightedDuplicate)
        Draw Red dot on top left corner

    // Only check for uniqueness if hovered (this avoid a O(n^2) search at each frame)
    if (id == ImGui::GetHoveredID())
    {
        if (g.UidsThisFrame.contains(id) && !g.UidWasTipDisplayed)
        {
            // Prepare highlight on the next frame for widgets that use this ID
            g.UidHighlightedDuplicate = id;
            g.UidHighlightedTimestamp = ImGui::GetFrameCount();
            if (ImGui::BeginTooltip())
            {
                if (! g.DebugItemPickerActive)
                {
                    // Display tooltip showing the duplicate Id as a string
                    ...
                    // Show hints for correction (PushID, ##)
                    ...

                    ImGui::Text("    Press Ctrl-P to break in any of those items    ");
                    if Ctrl-P
                        g.DebugItemPickerActive = true;
                }
                else
                {
                    // Add additional info at the bottom of the ItemPicker tooltip
                    ImGui::Text("Click on one of the widgets which uses a duplicated item");
                }
                ImGui::EndTooltip();
            }
            g.UidWasTipDisplayed = true;
        }
    }
    g.UidsThisFrame.push_back(id);
    return id;
}
```

## imgui_widgets.cpp

**Use ReserveUniqueID, allow duplicates when needed (TempInputText, BeginMenuEx)**
I went through all the usages of window->GetID() and changed them to ReserveUniqueID when it was possible / desirable.

### Places with one-line changes

In all the places below, the change was extremely simple: I simply replaced `window->GetID` by `window->ReserveUniqueID`.

For example, in ImGui::ButtonEx
```cpp
bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
    ...
    const ImGuiID id = window->ReserveUniqueID(label);  // Instead of window->GetID
```

Below is the list of all function where this simple change was applied:
```cpp
    InvisibleButton
    ArrowButtonEx
    GetWindowScrollbarID
    ImageButton  (both versions)
    Checkbox
    RadioButton
    BeginCombo
    DragScalar
    SliderScalar
    VSliderScalar
    InputTextEx
    ColorButton
    CollapsingHeader
    Selectable
    PlotEx
    BeginTabBar
    TextLink
    TreeNode(const char *)
    TreeNodeEx(const char *)
    TreeNodeExV(const char *)
```

### Places where duplicates are needed

#### TempInputText

TempInputText create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets)
It reuses the widget ID, so we allow it temporarily via PushAllowDuplicateID / PopAllowDuplicateID
```
bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
{
    ImGui::PushAllowDuplicateID();
    ...
    ImGui::PopAllowDuplicateID();
}

```

#### GetWindowScrollbarID
GetWindowScrollbarID had to be split in two: ReserveWindowScrollbarID will perform the ID reservation, while GetWindowScrollbarID is only a query
Luckily, this is the only instance where this was required.

#### BeginMenuEx

In BeginMenuEx, we initially call GetID() instead of ReserveUniqueID() because were are not "consuming" this ID:
instead, we are only querying IsPopupOpen().
The ID will be consumed later by calling ImGui::PushID(label) + Selectable("") (which will lead to the same ID)

```
bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
{
    const ImGuiID id = window->GetID(label);
    bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None);

    ...

    PushID(label);
    pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, label_size.y));

    ...
}
```

### Places with no changes, but worth mentioning

* TreeNode(const void *): unchanged
`TreeNode(const void* ptr_id)` and `TreeNodeExV(const void *)` are the only instances which call `window->GetID(const void *)`.
As a consequence, they cannot use the unique id mechanism, but I think their usage is rare enough that is not a big concern.

* ImGui::TabBarCalcTabID: unchanged
It still uses window->GetID, to avoid issues when merging with the docking branch.

* BeginChild, BeginPopup and co are unchanged:
  since they use ImGui::Begin()/End() internally), I suspect they might be called multiple times with the same string,
  since it is allowed with ImGui::Begin/End.

## imgui_demo.cpp: some minor changes

I went through the full demo Window to look for instances where a duplicate ID might be used, and corrected them.
Very few changes were required:

* **"Layout/Text Baseline Alignment"**: simple ID fix  (Added ##)
* **"Widgets/Plotting"**: simple id fix (Added ##)
* **"Columns (legacy API)/Borders"**: added ImGui::PushID for each cell
* **"Widgets/Drag and Drop/Drag to reorder items (simple)"**: no change, but a transient duplicate ID may appear during reordering
    (for *one* frame only: see comments in the code)

# Demo code in order to test this PR

Add and call this function in any imgui example. This code will trigger the warnings for duplicated IDs,
and will also enable to make sure that the fixes that had to be done are OK.

```cpp
char text1[500] = "Hello,";
char text2[500] = "world!";
char text3[500] = "Hello,";
char text4[500] = "world!";
int value = 0;
bool flag1 = false, flag2 = false;
float f1 = 0.f, f2 = 0.f, f3 = 0.f, f4 = 0.f;

void Gui()
{
    // Some widgets to show the duplicated ID warnings
    ImGui::SetNextWindowSize(ImVec2(400, 500));
    if (ImGui::Begin("Duplicated ID examples", nullptr, ImGuiWindowFlags_MenuBar))
    {
        ImGui::TextWrapped("Example of widgets using duplicated IDs");
        ImGui::Separator();

        ImGui::TextWrapped("Only the first button is clickable (same ID)");
        if (ImGui::Button("button")) printf("Button1 pressed\n");
        if (ImGui::Button("button")) printf("Button2 pressed\n");

        ImGui::Separator();

        ImGui::TextWrapped("Those text inputs will share the same value (editing one will overwrite the other)");
        ImGui::InputText("text", text1, IM_ARRAYSIZE(text1));
        ImGui::InputText("text", text2, IM_ARRAYSIZE(text2));

        ImGui::Separator();

        ImGui::TextWrapped("Those radio buttons will share the same value (selecting one will overwrite the other)");
        ImGui::RadioButton("radio", &value, 0);
        ImGui::RadioButton("radio", &value, 1);

        ImGui::Separator();

        ImGui::TextWrapped("The second checkbox cannot be toggled");
        ImGui::Checkbox("checkbox", &flag1);
        ImGui::Checkbox("checkbox", &flag2);

        ImGui::Separator();

        ImGui::TextWrapped("Those two sliders will share the same value (editing one will overwrite the other");
        ImGui::SliderFloat("slider", &f1, 0.f, 1.f);
        ImGui::SliderFloat("slider", &f2, 0.f, 1.f);

        ImGui::Separator();
    }
    ImGui::End();

    // Some widgets to test the fixes that had to be done
    ImGui::SetNextWindowSize(ImVec2(400, 400));
    if (ImGui::Begin("Regression tests", nullptr, ImGuiWindowFlags_MenuBar))
    {
        ImGui::TextWrapped("Small issues had to be fixed inside DragFloat (cf TempInputText), InputTextMultiline and BeginMenuBar.\n"
            "This window is here to test those fixes.");

        ImGui::Separator();

        if (ImGui::BeginMenuBar())
        {
            if (ImGui::BeginMenu("My Menu"))
            {
                ImGui::MenuItem("Item 1");
                ImGui::EndMenu();
            }
            ImGui::EndMenuBar();
        }

        ImGui::TextWrapped("DragFloat will reuse the Item ID when double clicking. This is allowed via Push/PopAllowDuplicateID");
        ImGui::DragFloat("dragf", &f3);

        ImGui::Separator();

        ImGui::TextWrapped("An issue was fixed with the InputTextMultiline vertical scrollbar, which trigerred a duplicate ID warning, which was fixed");
        ImGui::InputTextMultiline("multiline", text1, 500);

    }
    ImGui::End();

    // A window that is populated in several steps should still detect duplicates
    {
        ImGui::SetNextWindowSize(ImVec2(400, 200));

        ImGui::Begin("Window populated in several steps");
        ImGui::TextWrapped("This window is populated between several ImGui::Begin/ImGui::End calls (with the same window name).\n"
                           "Duplicates should be detected anyhow.");
        ImGui::Text("A first InputText");
        ImGui::InputText("TTT", text3, 500);
        ImGui::End();

        ImGui::Begin("Window populated in several steps");
        ImGui::Separator();
        ImGui::Text("The second InputText below reuses the same ID, \nin a different ImGui::Begin/ImGui::End context \n(but with the same window name)");
        ImGui::InputText("TTT", text4, 500);
        ImGui::End();
    }
}
```
pthom added a commit to pthom/imgui that referenced this issue Sep 6, 2024
(fix for ocornut#7669)

There is a common user issue which is that developers may reuse the same ID, and will not understand why their widget is not responsive.

This PR will:
* detect when the same ID is used on different widgets (only if the widget is hovered, to avoid a O(n^2) search at each frame)
* display a warning tooltip to the user, with hints on how to fix the issue (e.g. use PushID, or add a ## to the label)
* allow the user to break in the debugger on any of the widgets that use the same ID
* enable to disable the warning when needed (e.g. when reusing the ID is intentional, via PushAllowDuplicateID / PopAllowDuplicateID)
* enable to debug all duplicates at once (by defining IMGUI_DEBUG_PARANOID)

**View from 10,000 feet**

This PR adds a distinction between

```cpp
// Same as before
ImGuiID     GetID(const char* str, const char* str_end = NULL);
 ```
and
```cpp
// returns GetID(), but may display a warning tooltip if the ID is not unique. Should be called only once per frame with a given ID stack.
ImGuiID     ReserveUniqueID(const char* str_id);
```

This distinction was applied to several widgets inside imgui_widgets.cpp which now call ReserveUniqueID instead of GetID,
when their intention is to reserve a unique ID for the widget.

See this video for a quick overview of the feature and how it is implemented:

https://traineq.org/ImGui/ImGui_PR_Warn_reuse_ID_Explanation.mp4

> _Note: if desired, a compile flag (e.g. IMGUI_NO_WARN_DUPLICATE_ID or its inverse) could be added to disable the warning and make ReserveUniqueID behave like GetID()_

# Summary of changes

## struct ImGuiContext: added fields UidIXXX (UidsThisFrame & co)

```cpp
struct ImGuiContext
{
    ...
    // Declaration
    ImVector<ImGuiID>       UidsThisFrame;                      // A list of item IDs submitted this frame (used to detect and warn about duplicate ID usage).
    int                     UidAllowDuplicatesStack;            // Stack depth for allowing duplicate IDs (if > 0, duplicate IDs are allowed). See PushAllowDuplicateID / PopAllowDuplicateID
    ImGuiID                 UidHighlightedDuplicate;            // Will be set if a duplicated item is hovered (all duplicated items will appear with a red dot in the top left corner on the next frame)
    int                     UidHighlightedTimestamp;            // Timestamp (FrameCount) of the highlight (which will be shown on the next frame)
    bool                    UidWasTipDisplayed;                 // Will be set to true to avoid displaying multiple times the same tooltip
    ...

    // Constructor
    ImGuiContext(ImFontAtlas* shared_font_atlas) {
        ...
        UidsThisFrame.clear();
        UidAllowDuplicatesStack = 0;
        UidHighlightedDuplicate = 0;
        UidHighlightedTimestamp = 0;
        UidWasTipDisplayed = false;
        ...
    }
};
```

## struct ImGuiWindow: added ReserveUniqueID(), beside GetID()

imgui_internal.h / near GetID() existing methods
```cpp
struct ImGuiWindow {
    ...
    // ReserveUniqueID: returns GetID(), but may display a warning. Should be called only once per frame with a given ID
    ImGuiID     ReserveUniqueID(const char* str_id);
    ...
};
```

## Added PushAllowDuplicateID / PopAllowDuplicateID

imgui_internal.h  / near related ID functions (SetActiveID, GetIDWithSeed, ...)
```cpp
    ...
    IMGUI_API void          PushAllowDuplicateID();         // Disables the tooltip warning when duplicate IDs are detected.
    IMGUI_API void          PopAllowDuplicateID();          // Re-enables the tooltip warning when duplicate IDs are detected.
    ...
```

imgui.cpp:3873 / near ReserveUniqueID() implementation
```cpp
IMGUI_API void ImGui::PushAllowDuplicateID()
{
    ImGuiContext& g = *GImGui;
    g.UidAllowDuplicatesStack++;
}

IMGUI_API void ImGui::PopAllowDuplicateID()
{
    ImGuiContext& g = *GImGui;
    IM_ASSERT(g.UidAllowDuplicatesStack > 0 && "Mismatched PopAllowDuplicateID()");
    g.UidAllowDuplicatesStack--;
}
```

> _Note: doing this via ItemFlags was not feasible as many items (e.g. Button, Checkbox) do not expose flags_

## Main logic: ImGui::EndFrame(), NewFrame() and ImGuiWindow::ReserveUniqueID()

### ImGui::NewFrame():
```cpp
    ...
    g.UidsThisFrame.resize(0);  // Empty the list of items, but keep its allocated data
    g.UidWasTipDisplayed = false;
```

### ImGui::EndFrame():
```cpp
    IM_ASSERT(g.UidAllowDuplicatesStack == 0 && "Mismatched Push/PopAllowDuplicateID");
    if (g.UidHighlightedTimestamp != ImGui::GetFrameCount())
    {
        g.UidHighlightedTimestamp = 0;
        g.UidHighlightedDuplicate = 0;
    }
```

### ImGuiWindow::ReserveUniqueID()
in imgui.cpp, at the end of "[SECTION] ID STACK"

The pseudocode below summarizes its behavior:
```cpp
IMGUI_API ImGuiID ImGuiWindow::ReserveUniqueID(const char* str_id)
{
    ImGuiContext& g = *GImGui;
    ImGuiID id = GetID(str_id);

    // If PushAllowDuplicateID was called, do not check for uniqueness
    if (g.UidAllowDuplicatesStack != 0)
    return id;

    // Visual aid: a red circle in the top-left corner of all widgets that use a duplicate of the id
    if (id == g.UidHighlightedDuplicate)
        Draw Red dot on top left corner

    // Only check for uniqueness if hovered (this avoid a O(n^2) search at each frame)
    if (id == ImGui::GetHoveredID())
    {
        if (g.UidsThisFrame.contains(id) && !g.UidWasTipDisplayed)
        {
            // Prepare highlight on the next frame for widgets that use this ID
            g.UidHighlightedDuplicate = id;
            g.UidHighlightedTimestamp = ImGui::GetFrameCount();
            if (ImGui::BeginTooltip())
            {
                if (! g.DebugItemPickerActive)
                {
                    // Display tooltip showing the duplicate Id as a string
                    ...
                    // Show hints for correction (PushID, ##)
                    ...

                    ImGui::Text("    Press Ctrl-P to break in any of those items    ");
                    if Ctrl-P
                        g.DebugItemPickerActive = true;
                }
                else
                {
                    // Add additional info at the bottom of the ItemPicker tooltip
                    ImGui::Text("Click on one of the widgets which uses a duplicated item");
                }
                ImGui::EndTooltip();
            }
            g.UidWasTipDisplayed = true;
        }
    }
    g.UidsThisFrame.push_back(id);
    return id;
}
```

## imgui_widgets.cpp

**Use ReserveUniqueID, allow duplicates when needed (TempInputText, BeginMenuEx)**
I went through all the usages of window->GetID() and changed them to ReserveUniqueID when it was possible / desirable.

### Places with one-line changes

In all the places below, the change was extremely simple: I simply replaced `window->GetID` by `window->ReserveUniqueID`.

For example, in ImGui::ButtonEx
```cpp
bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
    ...
    const ImGuiID id = window->ReserveUniqueID(label);  // Instead of window->GetID
```

Below is the list of all function where this simple change was applied:
```cpp
    InvisibleButton
    ArrowButtonEx
    GetWindowScrollbarID
    ImageButton  (both versions)
    Checkbox
    RadioButton
    BeginCombo
    DragScalar
    SliderScalar
    VSliderScalar
    InputTextEx
    ColorButton
    CollapsingHeader
    Selectable
    PlotEx
    BeginTabBar
    TextLink
    TreeNode(const char *)
    TreeNodeEx(const char *)
    TreeNodeExV(const char *)
```

### Places where duplicates are needed

#### TempInputText

TempInputText create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets)
It reuses the widget ID, so we allow it temporarily via PushAllowDuplicateID / PopAllowDuplicateID
```
bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
{
    ImGui::PushAllowDuplicateID();
    ...
    ImGui::PopAllowDuplicateID();
}

```

#### GetWindowScrollbarID
GetWindowScrollbarID had to be split in two: ReserveWindowScrollbarID will perform the ID reservation, while GetWindowScrollbarID is only a query
Luckily, this is the only instance where this was required.

#### BeginMenuEx

In BeginMenuEx, we initially call GetID() instead of ReserveUniqueID() because were are not "consuming" this ID:
instead, we are only querying IsPopupOpen().
The ID will be consumed later by calling ImGui::PushID(label) + Selectable("") (which will lead to the same ID)

```
bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
{
    const ImGuiID id = window->GetID(label);
    bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None);

    ...

    PushID(label);
    pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, label_size.y));

    ...
}
```

### Places with no changes, but worth mentioning

* TreeNode(const void *): unchanged
`TreeNode(const void* ptr_id)` and `TreeNodeExV(const void *)` are the only instances which call `window->GetID(const void *)`.
As a consequence, they cannot use the unique id mechanism, but I think their usage is rare enough that is not a big concern.

* ImGui::TabBarCalcTabID: unchanged
It still uses window->GetID, to avoid issues when merging with the docking branch.

* BeginChild, BeginPopup and co are unchanged:
  since they use ImGui::Begin()/End() internally), I suspect they might be called multiple times with the same string,
  since it is allowed with ImGui::Begin/End.

## imgui_demo.cpp: some minor changes

I went through the full demo Window to look for instances where a duplicate ID might be used, and corrected them.
Very few changes were required:

* **"Layout/Text Baseline Alignment"**: simple ID fix  (Added ##)
* **"Widgets/Plotting"**: simple id fix (Added ##)
* **"Columns (legacy API)/Borders"**: added ImGui::PushID for each cell
* **"Widgets/Drag and Drop/Drag to reorder items (simple)"**: no change, but a transient duplicate ID may appear during reordering
    (for *one* frame only: see comments in the code)

# Demo code in order to test this PR

Add and call this function in any imgui example. This code will trigger the warnings for duplicated IDs,
and will also enable to make sure that the fixes that had to be done are OK.

```cpp
char text1[500] = "Hello,";
char text2[500] = "world!";
char text3[500] = "Hello,";
char text4[500] = "world!";
int value = 0;
bool flag1 = false, flag2 = false;
float f1 = 0.f, f2 = 0.f, f3 = 0.f, f4 = 0.f;

void Gui()
{
    // Some widgets to show the duplicated ID warnings
    ImGui::SetNextWindowSize(ImVec2(400, 500));
    if (ImGui::Begin("Duplicated ID examples", nullptr, ImGuiWindowFlags_MenuBar))
    {
        ImGui::TextWrapped("Example of widgets using duplicated IDs");
        ImGui::Separator();

        ImGui::TextWrapped("Only the first button is clickable (same ID)");
        if (ImGui::Button("button")) printf("Button1 pressed\n");
        if (ImGui::Button("button")) printf("Button2 pressed\n");

        ImGui::Separator();

        ImGui::TextWrapped("Those text inputs will share the same value (editing one will overwrite the other)");
        ImGui::InputText("text", text1, IM_ARRAYSIZE(text1));
        ImGui::InputText("text", text2, IM_ARRAYSIZE(text2));

        ImGui::Separator();

        ImGui::TextWrapped("Those radio buttons will share the same value (selecting one will overwrite the other)");
        ImGui::RadioButton("radio", &value, 0);
        ImGui::RadioButton("radio", &value, 1);

        ImGui::Separator();

        ImGui::TextWrapped("The second checkbox cannot be toggled");
        ImGui::Checkbox("checkbox", &flag1);
        ImGui::Checkbox("checkbox", &flag2);

        ImGui::Separator();

        ImGui::TextWrapped("Those two sliders will share the same value (editing one will overwrite the other");
        ImGui::SliderFloat("slider", &f1, 0.f, 1.f);
        ImGui::SliderFloat("slider", &f2, 0.f, 1.f);

        ImGui::Separator();
    }
    ImGui::End();

    // Some widgets to test the fixes that had to be done
    ImGui::SetNextWindowSize(ImVec2(400, 400));
    if (ImGui::Begin("Regression tests", nullptr, ImGuiWindowFlags_MenuBar))
    {
        ImGui::TextWrapped("Small issues had to be fixed inside DragFloat (cf TempInputText), InputTextMultiline and BeginMenuBar.\n"
            "This window is here to test those fixes.");

        ImGui::Separator();

        if (ImGui::BeginMenuBar())
        {
            if (ImGui::BeginMenu("My Menu"))
            {
                ImGui::MenuItem("Item 1");
                ImGui::EndMenu();
            }
            ImGui::EndMenuBar();
        }

        ImGui::TextWrapped("DragFloat will reuse the Item ID when double clicking. This is allowed via Push/PopAllowDuplicateID");
        ImGui::DragFloat("dragf", &f3);

        ImGui::Separator();

        ImGui::TextWrapped("An issue was fixed with the InputTextMultiline vertical scrollbar, which trigerred a duplicate ID warning, which was fixed");
        ImGui::InputTextMultiline("multiline", text1, 500);

    }
    ImGui::End();

    // A window that is populated in several steps should still detect duplicates
    {
        ImGui::SetNextWindowSize(ImVec2(400, 200));

        ImGui::Begin("Window populated in several steps");
        ImGui::TextWrapped("This window is populated between several ImGui::Begin/ImGui::End calls (with the same window name).\n"
                           "Duplicates should be detected anyhow.");
        ImGui::Text("A first InputText");
        ImGui::InputText("TTT", text3, 500);
        ImGui::End();

        ImGui::Begin("Window populated in several steps");
        ImGui::Separator();
        ImGui::Text("The second InputText below reuses the same ID, \nin a different ImGui::Begin/ImGui::End context \n(but with the same window name)");
        ImGui::InputText("TTT", text4, 500);
        ImGui::End();
    }
}
```
pthom added a commit to pthom/imgui that referenced this issue Sep 6, 2024
(fix for ocornut#7669)

There is a common user issue which is that developers may reuse the same ID, and will not understand why their widget is not responsive.

This PR will:
* detect when the same ID is used on different widgets (only if the widget is hovered, to avoid a O(n^2) search at each frame)
* display a warning tooltip to the user, with hints on how to fix the issue (e.g. use PushID, or add a ## to the label)
* allow the user to break in the debugger on any of the widgets that use the same ID
* enable to disable the warning when needed (e.g. when reusing the ID is intentional, via PushAllowDuplicateID / PopAllowDuplicateID)
* enable to debug all duplicates at once (by defining IMGUI_DEBUG_PARANOID)

**View from 10,000 feet**

1. This PR adds a distinction between

```cpp
// Same as before
ImGuiID     GetID(const char* str, const char* str_end = NULL);
 ```
and
```cpp
// returns GetID(), but may display a warning tooltip if the ID is not unique. Should be called only once per frame with a given ID stack.
ImGuiID     ReserveUniqueID(const char* str_id);
```

This distinction was applied to several widgets inside imgui_widgets.cpp which now call ReserveUniqueID instead of GetID,
when their intention is to reserve a unique ID for the widget.

2. For grepability, most of the changes are highlighted with a comment "// TRKID".
   The only changes which are not marked with this are where GetID() was simply replace by ReserveUniqueID() inside imgui_widgets.cpp
   These "// TRKID" comments should be removed before merging, and they are only here to help reviewing the changes.

3.See this video for a quick overview of the feature and how it is implemented:

https://traineq.org/ImGui/ImGui_PR_Warn_reuse_ID_Explanation.mp4

> _Note: if desired, a compile flag (e.g. IMGUI_NO_WARN_DUPLICATE_ID or its inverse) could be added to disable the warning and make ReserveUniqueID behave like GetID()_

# Summary of changes

## struct ImGuiContext: added fields UidIXXX (UidsThisFrame & co)

```cpp
struct ImGuiContext
{
    ...
    // Declaration
    ImVector<ImGuiID>       UidsThisFrame;                      // A list of item IDs submitted this frame (used to detect and warn about duplicate ID usage).
    int                     UidAllowDuplicatesStack;            // Stack depth for allowing duplicate IDs (if > 0, duplicate IDs are allowed). See PushAllowDuplicateID / PopAllowDuplicateID
    ImGuiID                 UidHighlightedDuplicate;            // Will be set if a duplicated item is hovered (all duplicated items will appear with a red dot in the top left corner on the next frame)
    int                     UidHighlightedTimestamp;            // Timestamp (FrameCount) of the highlight (which will be shown on the next frame)
    bool                    UidWasTipDisplayed;                 // Will be set to true to avoid displaying multiple times the same tooltip
    ...

    // Constructor
    ImGuiContext(ImFontAtlas* shared_font_atlas) {
        ...
        UidsThisFrame.clear();
        UidAllowDuplicatesStack = 0;
        UidHighlightedDuplicate = 0;
        UidHighlightedTimestamp = 0;
        UidWasTipDisplayed = false;
        ...
    }
};
```

## struct ImGuiWindow: added ReserveUniqueID(), beside GetID()

imgui_internal.h / near GetID() existing methods
```cpp
struct ImGuiWindow {
    ...
    // ReserveUniqueID: returns GetID(), but may display a warning. Should be called only once per frame with a given ID
    ImGuiID     ReserveUniqueID(const char* str_id);
    ...
};
```

## Added PushAllowDuplicateID / PopAllowDuplicateID

imgui_internal.h  / near related ID functions (SetActiveID, GetIDWithSeed, ...)
```cpp
    ...
    IMGUI_API void          PushAllowDuplicateID();         // Disables the tooltip warning when duplicate IDs are detected.
    IMGUI_API void          PopAllowDuplicateID();          // Re-enables the tooltip warning when duplicate IDs are detected.
    ...
```

imgui.cpp:3873 / near ReserveUniqueID() implementation
```cpp
IMGUI_API void ImGui::PushAllowDuplicateID()
{
    ImGuiContext& g = *GImGui;
    g.UidAllowDuplicatesStack++;
}

IMGUI_API void ImGui::PopAllowDuplicateID()
{
    ImGuiContext& g = *GImGui;
    IM_ASSERT(g.UidAllowDuplicatesStack > 0 && "Mismatched PopAllowDuplicateID()");
    g.UidAllowDuplicatesStack--;
}
```

> _Note: doing this via ItemFlags was not feasible as many items (e.g. Button, Checkbox) do not expose flags_

## Main logic: ImGui::EndFrame(), NewFrame() and ImGuiWindow::ReserveUniqueID()

### ImGui::NewFrame():
```cpp
    ...
    g.UidsThisFrame.resize(0);  // Empty the list of items, but keep its allocated data
    g.UidWasTipDisplayed = false;
```

### ImGui::EndFrame():
```cpp
    IM_ASSERT(g.UidAllowDuplicatesStack == 0 && "Mismatched Push/PopAllowDuplicateID");
    if (g.UidHighlightedTimestamp != ImGui::GetFrameCount())
    {
        g.UidHighlightedTimestamp = 0;
        g.UidHighlightedDuplicate = 0;
    }
```

### ImGuiWindow::ReserveUniqueID()
in imgui.cpp, at the end of "[SECTION] ID STACK"

The pseudocode below summarizes its behavior:
```cpp
IMGUI_API ImGuiID ImGuiWindow::ReserveUniqueID(const char* str_id)
{
    ImGuiContext& g = *GImGui;
    ImGuiID id = GetID(str_id);

    // If PushAllowDuplicateID was called, do not check for uniqueness
    if (g.UidAllowDuplicatesStack != 0)
    return id;

    // Visual aid: a red circle in the top-left corner of all widgets that use a duplicate of the id
    if (id == g.UidHighlightedDuplicate)
        Draw Red dot on top left corner

    // Only check for uniqueness if hovered (this avoid a O(n^2) search at each frame)
    if (id == ImGui::GetHoveredID())
    {
        if (g.UidsThisFrame.contains(id) && !g.UidWasTipDisplayed)
        {
            // Prepare highlight on the next frame for widgets that use this ID
            g.UidHighlightedDuplicate = id;
            g.UidHighlightedTimestamp = ImGui::GetFrameCount();
            if (ImGui::BeginTooltip())
            {
                if (! g.DebugItemPickerActive)
                {
                    // Display tooltip showing the duplicate Id as a string
                    ...
                    // Show hints for correction (PushID, ##)
                    ...

                    ImGui::Text("    Press Ctrl-P to break in any of those items    ");
                    if Ctrl-P
                        g.DebugItemPickerActive = true;
                }
                else
                {
                    // Add additional info at the bottom of the ItemPicker tooltip
                    ImGui::Text("Click on one of the widgets which uses a duplicated item");
                }
                ImGui::EndTooltip();
            }
            g.UidWasTipDisplayed = true;
        }
    }
    g.UidsThisFrame.push_back(id);
    return id;
}
```

## imgui_widgets.cpp

**Use ReserveUniqueID, allow duplicates when needed (TempInputText, BeginMenuEx)**
I went through all the usages of window->GetID() and changed them to ReserveUniqueID when it was possible / desirable.

### Places with one-line changes

In all the places below, the change was extremely simple: I simply replaced `window->GetID` by `window->ReserveUniqueID`.

For example, in ImGui::ButtonEx
```cpp
bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
    ...
    const ImGuiID id = window->ReserveUniqueID(label);  // Instead of window->GetID
```

Below is the list of all function where this simple change was applied:
```cpp
    InvisibleButton
    ArrowButtonEx
    GetWindowScrollbarID
    ImageButton  (both versions)
    Checkbox
    RadioButton
    BeginCombo
    DragScalar
    SliderScalar
    VSliderScalar
    InputTextEx
    ColorButton
    CollapsingHeader
    Selectable
    PlotEx
    BeginTabBar
    TextLink
    TreeNode(const char *)
    TreeNodeEx(const char *)
    TreeNodeExV(const char *)
```

### Places where duplicates are needed

#### TempInputText

TempInputText create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets)
It reuses the widget ID, so we allow it temporarily via PushAllowDuplicateID / PopAllowDuplicateID
```
bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
{
    ImGui::PushAllowDuplicateID();
    ...
    ImGui::PopAllowDuplicateID();
}

```

#### GetWindowScrollbarID
GetWindowScrollbarID had to be split in two: ReserveWindowScrollbarID will perform the ID reservation, while GetWindowScrollbarID is only a query
Luckily, this is the only instance where this was required.

#### BeginMenuEx

In BeginMenuEx, we initially call GetID() instead of ReserveUniqueID() because were are not "consuming" this ID:
instead, we are only querying IsPopupOpen().
The ID will be consumed later by calling ImGui::PushID(label) + Selectable("") (which will lead to the same ID)

```
bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
{
    const ImGuiID id = window->GetID(label);
    bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None);

    ...

    PushID(label);
    pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, label_size.y));

    ...
}
```

### Places with no changes, but worth mentioning

* TreeNode(const void *): unchanged
`TreeNode(const void* ptr_id)` and `TreeNodeExV(const void *)` are the only instances which call `window->GetID(const void *)`.
As a consequence, they cannot use the unique id mechanism, but I think their usage is rare enough that is not a big concern.

* ImGui::TabBarCalcTabID: unchanged
It still uses window->GetID, to avoid issues when merging with the docking branch.

* BeginChild, BeginPopup and co are unchanged:
  since they use ImGui::Begin()/End() internally), I suspect they might be called multiple times with the same string,
  since it is allowed with ImGui::Begin/End.

## imgui_demo.cpp: some minor changes

I went through the full demo Window to look for instances where a duplicate ID might be used, and corrected them.
Very few changes were required:

* **"Layout/Text Baseline Alignment"**: simple ID fix  (Added ##)
* **"Widgets/Plotting"**: simple id fix (Added ##)
* **"Columns (legacy API)/Borders"**: added ImGui::PushID for each cell
* **"Widgets/Drag and Drop/Drag to reorder items (simple)"**: no change, but a transient duplicate ID may appear during reordering
    (for *one* frame only: see comments in the code)

# Demo code in order to test this PR

Add and call this function in any imgui example. This code will trigger the warnings for duplicated IDs,
and will also enable to make sure that the fixes that had to be done are OK.

```cpp
char text1[500] = "Hello,";
char text2[500] = "world!";
char text3[500] = "Hello,";
char text4[500] = "world!";
int value = 0;
bool flag1 = false, flag2 = false;
float f1 = 0.f, f2 = 0.f, f3 = 0.f, f4 = 0.f;

void Gui()
{
    // Some widgets to show the duplicated ID warnings
    ImGui::SetNextWindowSize(ImVec2(400, 500));
    if (ImGui::Begin("Duplicated ID examples", nullptr, ImGuiWindowFlags_MenuBar))
    {
        ImGui::TextWrapped("Example of widgets using duplicated IDs");
        ImGui::Separator();

        ImGui::TextWrapped("Only the first button is clickable (same ID)");
        if (ImGui::Button("button")) printf("Button1 pressed\n");
        if (ImGui::Button("button")) printf("Button2 pressed\n");

        ImGui::Separator();

        ImGui::TextWrapped("Those text inputs will share the same value (editing one will overwrite the other)");
        ImGui::InputText("text", text1, IM_ARRAYSIZE(text1));
        ImGui::InputText("text", text2, IM_ARRAYSIZE(text2));

        ImGui::Separator();

        ImGui::TextWrapped("Those radio buttons will share the same value (selecting one will overwrite the other)");
        ImGui::RadioButton("radio", &value, 0);
        ImGui::RadioButton("radio", &value, 1);

        ImGui::Separator();

        ImGui::TextWrapped("The second checkbox cannot be toggled");
        ImGui::Checkbox("checkbox", &flag1);
        ImGui::Checkbox("checkbox", &flag2);

        ImGui::Separator();

        ImGui::TextWrapped("Those two sliders will share the same value (editing one will overwrite the other");
        ImGui::SliderFloat("slider", &f1, 0.f, 1.f);
        ImGui::SliderFloat("slider", &f2, 0.f, 1.f);

        ImGui::Separator();
    }
    ImGui::End();

    // Some widgets to test the fixes that had to be done
    ImGui::SetNextWindowSize(ImVec2(400, 400));
    if (ImGui::Begin("Regression tests", nullptr, ImGuiWindowFlags_MenuBar))
    {
        ImGui::TextWrapped("Small issues had to be fixed inside DragFloat (cf TempInputText), InputTextMultiline and BeginMenuBar.\n"
            "This window is here to test those fixes.");

        ImGui::Separator();

        if (ImGui::BeginMenuBar())
        {
            if (ImGui::BeginMenu("My Menu"))
            {
                ImGui::MenuItem("Item 1");
                ImGui::EndMenu();
            }
            ImGui::EndMenuBar();
        }

        ImGui::TextWrapped("DragFloat will reuse the Item ID when double clicking. This is allowed via Push/PopAllowDuplicateID");
        ImGui::DragFloat("dragf", &f3);

        ImGui::Separator();

        ImGui::TextWrapped("An issue was fixed with the InputTextMultiline vertical scrollbar, which trigerred a duplicate ID warning, which was fixed");
        ImGui::InputTextMultiline("multiline", text1, 500);

    }
    ImGui::End();

    // A window that is populated in several steps should still detect duplicates
    {
        ImGui::SetNextWindowSize(ImVec2(400, 200));

        ImGui::Begin("Window populated in several steps");
        ImGui::TextWrapped("This window is populated between several ImGui::Begin/ImGui::End calls (with the same window name).\n"
                           "Duplicates should be detected anyhow.");
        ImGui::Text("A first InputText");
        ImGui::InputText("TTT", text3, 500);
        ImGui::End();

        ImGui::Begin("Window populated in several steps");
        ImGui::Separator();
        ImGui::Text("The second InputText below reuses the same ID, \nin a different ImGui::Begin/ImGui::End context \n(but with the same window name)");
        ImGui::InputText("TTT", text4, 500);
        ImGui::End();
    }
}
```
pthom added a commit to pthom/imgui that referenced this issue Sep 6, 2024
(fix for ocornut#7669)

There is a common user issue which is that developers may reuse the same ID, and will not understand why their widget is not responsive.

This PR will:
* detect when the same ID is used on different widgets (only if the widget is hovered, to avoid a O(n^2) search at each frame)
* display a warning tooltip to the user, with hints on how to fix the issue (e.g. use PushID, or add a ## to the label)
* allow the user to break in the debugger on any of the widgets that use the same ID
* enable to disable the warning when needed (e.g. when reusing the ID is intentional, via PushAllowDuplicateID / PopAllowDuplicateID)
* enable to debug all duplicates at once (by defining IMGUI_DEBUG_PARANOID)

**View from 10,000 feet**

1. This PR adds a distinction between

```cpp
// Same as before
ImGuiID     GetID(const char* str, const char* str_end = NULL);
 ```
and
```cpp
// returns GetID(), but may display a warning tooltip if the ID is not unique. Should be called only once per frame with a given ID stack.
ImGuiID     ReserveUniqueID(const char* str_id);
```

This distinction was applied to several widgets inside imgui_widgets.cpp which now call ReserveUniqueID instead of GetID,
when their intention is to reserve a unique ID for the widget.

2. For grepability, most of the changes are highlighted with a comment `// TRKID`.
   The only changes which are not marked with this are where GetID() was simply replace by ReserveUniqueID() inside imgui_widgets.cpp
   These `// TRKID` comments should be removed before merging, and they are only here to help reviewing the changes.

3.See this video for a quick overview of the feature and how it is implemented:

https://traineq.org/ImGui/ImGui_PR_Warn_reuse_ID_Explanation.mp4

----

> _Note: if desired, a compile flag (e.g. IMGUI_NO_WARN_DUPLICATE_ID or its inverse) could be added to disable the warning and make ReserveUniqueID behave like GetID()_

Summary of changes
==================

struct ImGuiContext: added fields UidIXXX (UidsThisFrame & co)
--------------------------------------------------------------

```cpp
struct ImGuiContext
{
    ...
    // Declaration
    ImVector<ImGuiID>       UidsThisFrame;                      // A list of item IDs submitted this frame (used to detect and warn about duplicate ID usage).
    int                     UidAllowDuplicatesStack;            // Stack depth for allowing duplicate IDs (if > 0, duplicate IDs are allowed). See PushAllowDuplicateID / PopAllowDuplicateID
    ImGuiID                 UidHighlightedDuplicate;            // Will be set if a duplicated item is hovered (all duplicated items will appear with a red dot in the top left corner on the next frame)
    int                     UidHighlightedTimestamp;            // Timestamp (FrameCount) of the highlight (which will be shown on the next frame)
    bool                    UidWasTipDisplayed;                 // Will be set to true to avoid displaying multiple times the same tooltip
    ...

    // Constructor
    ImGuiContext(ImFontAtlas* shared_font_atlas) {
        ...
        UidsThisFrame.clear();
        UidAllowDuplicatesStack = 0;
        UidHighlightedDuplicate = 0;
        UidHighlightedTimestamp = 0;
        UidWasTipDisplayed = false;
        ...
    }
};
```

struct ImGuiWindow: added ReserveUniqueID(), beside GetID()
-----------------------------------------------------------

```cpp

imgui_internal.h / near GetID() existing methods
```cpp
struct ImGuiWindow {
    ...
    // ReserveUniqueID: returns GetID(), but may display a warning. Should be called only once per frame with a given ID
    ImGuiID     ReserveUniqueID(const char* str_id);
    ...
};
```

Added PushAllowDuplicateID / PopAllowDuplicateID
------------------------------------------------

```cpp

imgui_internal.h  / near related ID functions (SetActiveID, GetIDWithSeed, ...)
```cpp
    ...
    IMGUI_API void          PushAllowDuplicateID();         // Disables the tooltip warning when duplicate IDs are detected.
    IMGUI_API void          PopAllowDuplicateID();          // Re-enables the tooltip warning when duplicate IDs are detected.
    ...
```

imgui.cpp:3873 / near ReserveUniqueID() implementation
```cpp
IMGUI_API void ImGui::PushAllowDuplicateID()
{
    ImGuiContext& g = *GImGui;
    g.UidAllowDuplicatesStack++;
}

IMGUI_API void ImGui::PopAllowDuplicateID()
{
    ImGuiContext& g = *GImGui;
    IM_ASSERT(g.UidAllowDuplicatesStack > 0 && "Mismatched PopAllowDuplicateID()");
    g.UidAllowDuplicatesStack--;
}
```

> _Note: doing this via ItemFlags was not feasible as many items (e.g. Button, Checkbox) do not expose flags_

Main logic: ImGui::EndFrame(), NewFrame() and ImGuiWindow::ReserveUniqueID()
----------------------------------------------------------------------------

```cpp

### ImGui::NewFrame():

```cpp
    ...
    g.UidsThisFrame.resize(0);  // Empty the list of items, but keep its allocated data
    g.UidWasTipDisplayed = false;
```

### ImGui::EndFrame():
```cpp
    IM_ASSERT(g.UidAllowDuplicatesStack == 0 && "Mismatched Push/PopAllowDuplicateID");
    if (g.UidHighlightedTimestamp != ImGui::GetFrameCount())
    {
        g.UidHighlightedTimestamp = 0;
        g.UidHighlightedDuplicate = 0;
    }
```

### ImGuiWindow::ReserveUniqueID()
in imgui.cpp, at the end of "[SECTION] ID STACK"

The pseudocode below summarizes its behavior:
```cpp
IMGUI_API ImGuiID ImGuiWindow::ReserveUniqueID(const char* str_id)
{
    ImGuiContext& g = *GImGui;
    ImGuiID id = GetID(str_id);

    // If PushAllowDuplicateID was called, do not check for uniqueness
    if (g.UidAllowDuplicatesStack != 0)
    return id;

    // Visual aid: a red circle in the top-left corner of all widgets that use a duplicate of the id
    if (id == g.UidHighlightedDuplicate)
        Draw Red dot on top left corner

    // Only check for uniqueness if hovered (this avoid a O(n^2) search at each frame)
    if (id == ImGui::GetHoveredID())
    {
        if (g.UidsThisFrame.contains(id) && !g.UidWasTipDisplayed)
        {
            // Prepare highlight on the next frame for widgets that use this ID
            g.UidHighlightedDuplicate = id;
            g.UidHighlightedTimestamp = ImGui::GetFrameCount();
            if (ImGui::BeginTooltip())
            {
                if (! g.DebugItemPickerActive)
                {
                    // Display tooltip showing the duplicate Id as a string
                    ...
                    // Show hints for correction (PushID, ##)
                    ...

                    ImGui::Text("    Press Ctrl-P to break in any of those items    ");
                    if Ctrl-P
                        g.DebugItemPickerActive = true;
                }
                else
                {
                    // Add additional info at the bottom of the ItemPicker tooltip
                    ImGui::Text("Click on one of the widgets which uses a duplicated item");
                }
                ImGui::EndTooltip();
            }
            g.UidWasTipDisplayed = true;
        }
    }
    g.UidsThisFrame.push_back(id);
    return id;
}
```

imgui_widgets.cpp
-----------------

**Use ReserveUniqueID, allow duplicates when needed (TempInputText, BeginMenuEx)**
I went through all the usages of window->GetID() and changed them to ReserveUniqueID when it was possible / desirable.

### Places with one-line changes

In all the places below, the change was extremely simple: I simply replaced `window->GetID` by `window->ReserveUniqueID`.

For example, in ImGui::ButtonEx
```cpp
bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
    ...
    const ImGuiID id = window->ReserveUniqueID(label);  // Instead of window->GetID
```

Below is the list of all function where this simple change was applied:
```cpp
    InvisibleButton
    ArrowButtonEx
    GetWindowScrollbarID
    ImageButton  (both versions)
    Checkbox
    RadioButton
    BeginCombo
    DragScalar
    SliderScalar
    VSliderScalar
    InputTextEx
    ColorButton
    CollapsingHeader
    Selectable
    PlotEx
    BeginTabBar
    TextLink
    TreeNode(const char *)
    TreeNodeEx(const char *)
    TreeNodeExV(const char *)
```

### Places where duplicates are needed

#### TempInputText

TempInputText create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets)
It reuses the widget ID, so we allow it temporarily via PushAllowDuplicateID / PopAllowDuplicateID
```
bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
{
    ImGui::PushAllowDuplicateID();
    ...
    ImGui::PopAllowDuplicateID();
}

```

#### GetWindowScrollbarID
GetWindowScrollbarID had to be split in two: ReserveWindowScrollbarID will perform the ID reservation, while GetWindowScrollbarID is only a query
Luckily, this is the only instance where this was required.

#### BeginMenuEx

In BeginMenuEx, we initially call GetID() instead of ReserveUniqueID() because were are not "consuming" this ID:
instead, we are only querying IsPopupOpen().
The ID will be consumed later by calling ImGui::PushID(label) + Selectable("") (which will lead to the same ID)

```
bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
{
    const ImGuiID id = window->GetID(label);
    bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None);

    ...

    PushID(label);
    pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, label_size.y));

    ...
}
```

### Places with no changes, but worth mentioning

* TreeNode(const void *): unchanged
`TreeNode(const void* ptr_id)` and `TreeNodeExV(const void *)` are the only instances which call `window->GetID(const void *)`.
As a consequence, they cannot use the unique id mechanism, but I think their usage is rare enough that is not a big concern.

* ImGui::TabBarCalcTabID: unchanged
It still uses window->GetID, to avoid issues when merging with the docking branch.

* BeginChild, BeginPopup and co are unchanged:
  since they use ImGui::Begin()/End() internally), I suspect they might be called multiple times with the same string,
  since it is allowed with ImGui::Begin/End.

imgui_demo.cpp: some minor changes
----------------------------------

I went through the full demo Window to look for instances where a duplicate ID might be used, and corrected them.
Very few changes were required:

* **"Layout/Text Baseline Alignment"**: simple ID fix  (Added ##)
* **"Widgets/Plotting"**: simple id fix (Added ##)
* **"Columns (legacy API)/Borders"**: added ImGui::PushID for each cell
* **"Widgets/Drag and Drop/Drag to reorder items (simple)"**: no change, but a transient duplicate ID may appear during reordering
    (for *one* frame only: see comments in the code)

Demo code in order to test this PR
==================================

Add and call this function in any imgui example. This code will trigger the warnings for duplicated IDs,
and will also enable to make sure that the fixes that had to be done are OK.

```cpp
char text1[500] = "Hello,";
char text2[500] = "world!";
char text3[500] = "Hello,";
char text4[500] = "world!";
int value = 0;
bool flag1 = false, flag2 = false;
float f1 = 0.f, f2 = 0.f, f3 = 0.f, f4 = 0.f;

void Gui()
{
    // Some widgets to show the duplicated ID warnings
    ImGui::SetNextWindowSize(ImVec2(400, 500));
    if (ImGui::Begin("Duplicated ID examples", nullptr, ImGuiWindowFlags_MenuBar))
    {
        ImGui::TextWrapped("Example of widgets using duplicated IDs");
        ImGui::Separator();

        ImGui::TextWrapped("Only the first button is clickable (same ID)");
        if (ImGui::Button("button")) printf("Button1 pressed\n");
        if (ImGui::Button("button")) printf("Button2 pressed\n");

        ImGui::Separator();

        ImGui::TextWrapped("Those text inputs will share the same value (editing one will overwrite the other)");
        ImGui::InputText("text", text1, IM_ARRAYSIZE(text1));
        ImGui::InputText("text", text2, IM_ARRAYSIZE(text2));

        ImGui::Separator();

        ImGui::TextWrapped("Those radio buttons will share the same value (selecting one will overwrite the other)");
        ImGui::RadioButton("radio", &value, 0);
        ImGui::RadioButton("radio", &value, 1);

        ImGui::Separator();

        ImGui::TextWrapped("The second checkbox cannot be toggled");
        ImGui::Checkbox("checkbox", &flag1);
        ImGui::Checkbox("checkbox", &flag2);

        ImGui::Separator();

        ImGui::TextWrapped("Those two sliders will share the same value (editing one will overwrite the other");
        ImGui::SliderFloat("slider", &f1, 0.f, 1.f);
        ImGui::SliderFloat("slider", &f2, 0.f, 1.f);

        ImGui::Separator();
    }
    ImGui::End();

    // Some widgets to test the fixes that had to be done
    ImGui::SetNextWindowSize(ImVec2(400, 400));
    if (ImGui::Begin("Regression tests", nullptr, ImGuiWindowFlags_MenuBar))
    {
        ImGui::TextWrapped("Small issues had to be fixed inside DragFloat (cf TempInputText), InputTextMultiline and BeginMenuBar.\n"
            "This window is here to test those fixes.");

        ImGui::Separator();

        if (ImGui::BeginMenuBar())
        {
            if (ImGui::BeginMenu("My Menu"))
            {
                ImGui::MenuItem("Item 1");
                ImGui::EndMenu();
            }
            ImGui::EndMenuBar();
        }

        ImGui::TextWrapped("DragFloat will reuse the Item ID when double clicking. This is allowed via Push/PopAllowDuplicateID");
        ImGui::DragFloat("dragf", &f3);

        ImGui::Separator();

        ImGui::TextWrapped("An issue was fixed with the InputTextMultiline vertical scrollbar, which trigerred a duplicate ID warning, which was fixed");
        ImGui::InputTextMultiline("multiline", text1, 500);

    }
    ImGui::End();

    // A window that is populated in several steps should still detect duplicates
    {
        ImGui::SetNextWindowSize(ImVec2(400, 200));

        ImGui::Begin("Window populated in several steps");
        ImGui::TextWrapped("This window is populated between several ImGui::Begin/ImGui::End calls (with the same window name).\n"
                           "Duplicates should be detected anyhow.");
        ImGui::Text("A first InputText");
        ImGui::InputText("TTT", text3, 500);
        ImGui::End();

        ImGui::Begin("Window populated in several steps");
        ImGui::Separator();
        ImGui::Text("The second InputText below reuses the same ID, \nin a different ImGui::Begin/ImGui::End context \n(but with the same window name)");
        ImGui::InputText("TTT", text4, 500);
        ImGui::End();
    }
}
```
pthom added a commit to pthom/imgui that referenced this issue Sep 6, 2024
(fix for ocornut#7669)

There is a common user issue which is that developers may reuse the same ID, and will not understand why their widget is not responsive.

This PR will:
* detect when the same ID is used on different widgets (only if the widget is hovered, to avoid a O(n^2) search at each frame)
* display a warning tooltip to the user, with hints on how to fix the issue (e.g. use PushID, or add a ## to the label)
* allow the user to break in the debugger on any of the widgets that use the same ID
* enable to disable the warning when needed (e.g. when reusing the ID is intentional, via PushAllowDuplicateID / PopAllowDuplicateID)
* enable to debug all duplicates at once (by defining IMGUI_DEBUG_PARANOID)

**View from 10,000 feet**

1. This PR adds a distinction between

```cpp
// Same as before
ImGuiID     GetID(const char* str, const char* str_end = NULL);
 ```
and
```cpp
// returns GetID(), but may display a warning tooltip if the ID is not unique. Should be called only once per frame with a given ID stack.
ImGuiID     ReserveUniqueID(const char* str_id);
```

This distinction was applied to several widgets inside imgui_widgets.cpp which now call ReserveUniqueID instead of GetID,
when their intention is to reserve a unique ID for the widget.

2. For grepability, most of the changes are highlighted with a comment `// TRKID`.
   The only changes which are not marked with this are where GetID() was simply replace by ReserveUniqueID() inside imgui_widgets.cpp
   These `// TRKID` comments should be removed before merging, and they are only here to help reviewing the changes.

3.See this video for a quick overview of the feature and how it is implemented:

https://traineq.org/ImGui/ImGui_PR_Warn_reuse_ID_Explanation.mp4

----

> _Note: if desired, a compile flag (e.g. IMGUI_NO_WARN_DUPLICATE_ID or its inverse) could be added to disable the warning and make ReserveUniqueID behave like GetID()_

Summary of changes
==================

struct ImGuiContext: added fields UidIXXX (UidsThisFrame & co)
--------------------------------------------------------------

Inside imgui_internal.h / struct ImGuiContext
```cpp
struct ImGuiContext
{
    ...
    // Declaration
    ImVector<ImGuiID>       UidsThisFrame;                      // A list of item IDs submitted this frame (used to detect and warn about duplicate ID usage).
    int                     UidAllowDuplicatesStack;            // Stack depth for allowing duplicate IDs (if > 0, duplicate IDs are allowed). See PushAllowDuplicateID / PopAllowDuplicateID
    ImGuiID                 UidHighlightedDuplicate;            // Will be set if a duplicated item is hovered (all duplicated items will appear with a red dot in the top left corner on the next frame)
    int                     UidHighlightedTimestamp;            // Timestamp (FrameCount) of the highlight (which will be shown on the next frame)
    bool                    UidWasTipDisplayed;                 // Will be set to true to avoid displaying multiple times the same tooltip
    ...

    // Constructor
    ImGuiContext(ImFontAtlas* shared_font_atlas) {
        ...
        UidsThisFrame.clear();
        UidAllowDuplicatesStack = 0;
        UidHighlightedDuplicate = 0;
        UidHighlightedTimestamp = 0;
        UidWasTipDisplayed = false;
        ...
    }
};
```

struct ImGuiWindow: added ReserveUniqueID(), beside GetID()
-----------------------------------------------------------

imgui_internal.h / near GetID() existing methods
```cpp
struct ImGuiWindow {
    ...
    // ReserveUniqueID: returns GetID(), but may display a warning. Should be called only once per frame with a given ID
    ImGuiID     ReserveUniqueID(const char* str_id);
    ...
};
```

Added PushAllowDuplicateID / PopAllowDuplicateID
------------------------------------------------

imgui_internal.h  / near related ID functions (SetActiveID, GetIDWithSeed, ...)
```cpp
    ...
    IMGUI_API void          PushAllowDuplicateID();         // Disables the tooltip warning when duplicate IDs are detected.
    IMGUI_API void          PopAllowDuplicateID();          // Re-enables the tooltip warning when duplicate IDs are detected.
    ...
```

imgui.cpp:3873 / near ReserveUniqueID() implementation
```cpp
IMGUI_API void ImGui::PushAllowDuplicateID()
{
    ImGuiContext& g = *GImGui;
    g.UidAllowDuplicatesStack++;
}

IMGUI_API void ImGui::PopAllowDuplicateID()
{
    ImGuiContext& g = *GImGui;
    IM_ASSERT(g.UidAllowDuplicatesStack > 0 && "Mismatched PopAllowDuplicateID()");
    g.UidAllowDuplicatesStack--;
}
```

> _Note: doing this via ItemFlags was not feasible as many items (e.g. Button, Checkbox) do not expose flags_

Main logic: ImGui::EndFrame(), NewFrame() and ImGuiWindow::ReserveUniqueID()
----------------------------------------------------------------------------

### ImGui::NewFrame():

```cpp
    ...
    g.UidsThisFrame.resize(0);  // Empty the list of items, but keep its allocated data
    g.UidWasTipDisplayed = false;
```

### ImGui::EndFrame():
```cpp
    IM_ASSERT(g.UidAllowDuplicatesStack == 0 && "Mismatched Push/PopAllowDuplicateID");
    if (g.UidHighlightedTimestamp != ImGui::GetFrameCount())
    {
        g.UidHighlightedTimestamp = 0;
        g.UidHighlightedDuplicate = 0;
    }
```

### ImGuiWindow::ReserveUniqueID()
in imgui.cpp, at the end of "[SECTION] ID STACK".
The pseudocode below summarizes its behavior:

```cpp
IMGUI_API ImGuiID ImGuiWindow::ReserveUniqueID(const char* str_id)
{
    ImGuiContext& g = *GImGui;
    ImGuiID id = GetID(str_id);

    // If PushAllowDuplicateID was called, do not check for uniqueness
    if (g.UidAllowDuplicatesStack != 0)
    return id;

    // Visual aid: a red circle in the top-left corner of all widgets that use a duplicate of the id
    if (id == g.UidHighlightedDuplicate)
        Draw Red dot on top left corner

    // Only check for uniqueness if hovered (this avoid a O(n^2) search at each frame)
    if (id == ImGui::GetHoveredID())
    {
        if (g.UidsThisFrame.contains(id) && !g.UidWasTipDisplayed)
        {
            // Prepare highlight on the next frame for widgets that use this ID
            g.UidHighlightedDuplicate = id;
            g.UidHighlightedTimestamp = ImGui::GetFrameCount();
            if (ImGui::BeginTooltip())
            {
                if (! g.DebugItemPickerActive)
                {
                    // Display tooltip showing the duplicate Id as a string
                    ...
                    // Show hints for correction (PushID, ##)
                    ...

                    ImGui::Text("    Press Ctrl-P to break in any of those items    ");
                    if Ctrl-P
                        g.DebugItemPickerActive = true;
                }
                else
                {
                    // Add additional info at the bottom of the ItemPicker tooltip
                    ImGui::Text("Click on one of the widgets which uses a duplicated item");
                }
                ImGui::EndTooltip();
            }
            g.UidWasTipDisplayed = true;
        }
    }
    g.UidsThisFrame.push_back(id);
    return id;
}
```

imgui_widgets.cpp
-----------------

**Use ReserveUniqueID, allow duplicates when needed (TempInputText, BeginMenuEx)**
I went through all the usages of window->GetID() and changed them to ReserveUniqueID when it was possible / desirable.

### Places with one-line changes

In all the places below, the change was extremely simple: I simply replaced `window->GetID` by `window->ReserveUniqueID`.

For example, in ImGui::ButtonEx
```cpp
bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
    ...
    const ImGuiID id = window->ReserveUniqueID(label);  // Instead of window->GetID
```

Below is the list of all function where this simple change was applied:
```cpp
    InvisibleButton
    ArrowButtonEx
    GetWindowScrollbarID
    ImageButton  (both versions)
    Checkbox
    RadioButton
    BeginCombo
    DragScalar
    SliderScalar
    VSliderScalar
    InputTextEx
    ColorButton
    CollapsingHeader
    Selectable
    PlotEx
    BeginTabBar
    TextLink
    TreeNode(const char *)
    TreeNodeEx(const char *)
    TreeNodeExV(const char *)
```

### Places where duplicates are needed

#### TempInputText

TempInputText create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets)
It reuses the widget ID, so we allow it temporarily via PushAllowDuplicateID / PopAllowDuplicateID
```
bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
{
    ImGui::PushAllowDuplicateID();
    ...
    ImGui::PopAllowDuplicateID();
}

```

#### GetWindowScrollbarID
GetWindowScrollbarID had to be split in two: ReserveWindowScrollbarID will perform the ID reservation, while GetWindowScrollbarID is only a query
Luckily, this is the only instance where this was required.

#### BeginMenuEx

In BeginMenuEx, we initially call GetID() instead of ReserveUniqueID() because were are not "consuming" this ID:
instead, we are only querying IsPopupOpen().
The ID will be consumed later by calling ImGui::PushID(label) + Selectable("") (which will lead to the same ID)

```
bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
{
    const ImGuiID id = window->GetID(label);
    bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None);

    ...

    PushID(label);
    pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, label_size.y));

    ...
}
```

### Places with no changes, but worth mentioning

* TreeNode(const void *): unchanged
`TreeNode(const void* ptr_id)` and `TreeNodeExV(const void *)` are the only instances which call `window->GetID(const void *)`.
As a consequence, they cannot use the unique id mechanism, but I think their usage is rare enough that is not a big concern.

* ImGui::TabBarCalcTabID: unchanged
It still uses window->GetID, to avoid issues when merging with the docking branch.

* BeginChild, BeginPopup and co are unchanged:
  since they use ImGui::Begin()/End() internally), I suspect they might be called multiple times with the same string,
  since it is allowed with ImGui::Begin/End.

imgui_demo.cpp: some minor changes
----------------------------------

I went through the full demo Window to look for instances where a duplicate ID might be used, and corrected them.
Very few changes were required:

* **"Layout/Text Baseline Alignment"**: simple ID fix  (Added ##)
* **"Widgets/Plotting"**: simple id fix (Added ##)
* **"Columns (legacy API)/Borders"**: added ImGui::PushID for each cell
* **"Widgets/Drag and Drop/Drag to reorder items (simple)"**: no change, but a transient duplicate ID may appear during reordering
    (for *one* frame only: see comments in the code)

Demo code in order to test this PR
==================================

Add and call this function in any imgui example. This code will trigger the warnings for duplicated IDs,
and will also enable to make sure that the fixes that had to be done are OK.

```cpp
char text1[500] = "Hello,";
char text2[500] = "world!";
char text3[500] = "Hello,";
char text4[500] = "world!";
int value = 0;
bool flag1 = false, flag2 = false;
float f1 = 0.f, f2 = 0.f, f3 = 0.f, f4 = 0.f;

void Gui()
{
    // Some widgets to show the duplicated ID warnings
    ImGui::SetNextWindowSize(ImVec2(400, 500));
    if (ImGui::Begin("Duplicated ID examples", nullptr, ImGuiWindowFlags_MenuBar))
    {
        ImGui::TextWrapped("Example of widgets using duplicated IDs");
        ImGui::Separator();

        ImGui::TextWrapped("Only the first button is clickable (same ID)");
        if (ImGui::Button("button")) printf("Button1 pressed\n");
        if (ImGui::Button("button")) printf("Button2 pressed\n");

        ImGui::Separator();

        ImGui::TextWrapped("Those text inputs will share the same value (editing one will overwrite the other)");
        ImGui::InputText("text", text1, IM_ARRAYSIZE(text1));
        ImGui::InputText("text", text2, IM_ARRAYSIZE(text2));

        ImGui::Separator();

        ImGui::TextWrapped("Those radio buttons will share the same value (selecting one will overwrite the other)");
        ImGui::RadioButton("radio", &value, 0);
        ImGui::RadioButton("radio", &value, 1);

        ImGui::Separator();

        ImGui::TextWrapped("The second checkbox cannot be toggled");
        ImGui::Checkbox("checkbox", &flag1);
        ImGui::Checkbox("checkbox", &flag2);

        ImGui::Separator();

        ImGui::TextWrapped("Those two sliders will share the same value (editing one will overwrite the other");
        ImGui::SliderFloat("slider", &f1, 0.f, 1.f);
        ImGui::SliderFloat("slider", &f2, 0.f, 1.f);

        ImGui::Separator();
    }
    ImGui::End();

    // Some widgets to test the fixes that had to be done
    ImGui::SetNextWindowSize(ImVec2(400, 400));
    if (ImGui::Begin("Regression tests", nullptr, ImGuiWindowFlags_MenuBar))
    {
        ImGui::TextWrapped("Small issues had to be fixed inside DragFloat (cf TempInputText), InputTextMultiline and BeginMenuBar.\n"
            "This window is here to test those fixes.");

        ImGui::Separator();

        if (ImGui::BeginMenuBar())
        {
            if (ImGui::BeginMenu("My Menu"))
            {
                ImGui::MenuItem("Item 1");
                ImGui::EndMenu();
            }
            ImGui::EndMenuBar();
        }

        ImGui::TextWrapped("DragFloat will reuse the Item ID when double clicking. This is allowed via Push/PopAllowDuplicateID");
        ImGui::DragFloat("dragf", &f3);

        ImGui::Separator();

        ImGui::TextWrapped("An issue was fixed with the InputTextMultiline vertical scrollbar, which trigerred a duplicate ID warning, which was fixed");
        ImGui::InputTextMultiline("multiline", text1, 500);

    }
    ImGui::End();

    // A window that is populated in several steps should still detect duplicates
    {
        ImGui::SetNextWindowSize(ImVec2(400, 200));

        ImGui::Begin("Window populated in several steps");
        ImGui::TextWrapped("This window is populated between several ImGui::Begin/ImGui::End calls (with the same window name).\n"
                           "Duplicates should be detected anyhow.");
        ImGui::Text("A first InputText");
        ImGui::InputText("TTT", text3, 500);
        ImGui::End();

        ImGui::Begin("Window populated in several steps");
        ImGui::Separator();
        ImGui::Text("The second InputText below reuses the same ID, \nin a different ImGui::Begin/ImGui::End context \n(but with the same window name)");
        ImGui::InputText("TTT", text4, 500);
        ImGui::End();
    }
}
```
pthom added a commit to pthom/imgui that referenced this issue Sep 6, 2024
(fix for ocornut#7669)

There is a common user issue which is that developers may reuse the same ID, and will not understand why their widget is not responsive.

This PR will:
* detect when the same ID is used on different widgets (only if the widget is hovered, to avoid a O(n^2) search at each frame)
* display a warning tooltip to the user, with hints on how to fix the issue (e.g. use PushID, or add a ## to the label)
* allow the user to break in the debugger on any of the widgets that use the same ID
* enable to disable the warning when needed (e.g. when reusing the ID is intentional, via PushAllowDuplicateID / PopAllowDuplicateID)
* enable to debug all duplicates at once (by defining IMGUI_DEBUG_PARANOID)

**View from 10,000 feet**

1. This PR adds a distinction between

```cpp
// Same as before
ImGuiID     GetID(const char* str, const char* str_end = NULL);
 ```
and
```cpp
// returns GetID(), but may display a warning tooltip if the ID is not unique. Should be called only once per frame with a given ID stack.
ImGuiID     ReserveUniqueID(const char* str_id);
```

This distinction was applied to several widgets inside imgui_widgets.cpp which now call ReserveUniqueID instead of GetID,
when their intention is to reserve a unique ID for the widget.

2. For grepability, most of the changes are highlighted with a comment `// TRKID`.
   The only changes which are not marked with this are where GetID() was simply replace by ReserveUniqueID() inside imgui_widgets.cpp
   These `// TRKID` comments should be removed before merging, and they are only here to help reviewing the changes.

3. The full `ImGui::ShowDemoWindow()` was reviewed to make sure that no duplicated ID was used.
   Very few changes were required, and they are detailed in the PR.

4.See this video for a quick overview of the feature and how it is implemented:

https://traineq.org/ImGui/ImGui_PR_Warn_reuse_ID_Explanation.mp4

----

> _Note: if desired, a compile flag (e.g. IMGUI_NO_WARN_DUPLICATE_ID or its inverse) could be added to disable the warning and make ReserveUniqueID behave like GetID()_

Summary of changes
==================

struct ImGuiContext: added fields UidIXXX (UidsThisFrame & co)
--------------------------------------------------------------

Inside imgui_internal.h / struct ImGuiContext
```cpp
struct ImGuiContext
{
    ...
    // Declaration
    ImVector<ImGuiID>       UidsThisFrame;                      // A list of item IDs submitted this frame (used to detect and warn about duplicate ID usage).
    int                     UidAllowDuplicatesStack;            // Stack depth for allowing duplicate IDs (if > 0, duplicate IDs are allowed). See PushAllowDuplicateID / PopAllowDuplicateID
    ImGuiID                 UidHighlightedDuplicate;            // Will be set if a duplicated item is hovered (all duplicated items will appear with a red dot in the top left corner on the next frame)
    int                     UidHighlightedTimestamp;            // Timestamp (FrameCount) of the highlight (which will be shown on the next frame)
    bool                    UidWasTipDisplayed;                 // Will be set to true to avoid displaying multiple times the same tooltip
    ...

    // Constructor
    ImGuiContext(ImFontAtlas* shared_font_atlas) {
        ...
        UidsThisFrame.clear();
        UidAllowDuplicatesStack = 0;
        UidHighlightedDuplicate = 0;
        UidHighlightedTimestamp = 0;
        UidWasTipDisplayed = false;
        ...
    }
};
```

struct ImGuiWindow: added ReserveUniqueID(), beside GetID()
-----------------------------------------------------------

imgui_internal.h / near GetID() existing methods
```cpp
struct ImGuiWindow {
    ...
    // ReserveUniqueID: returns GetID(), but may display a warning. Should be called only once per frame with a given ID
    ImGuiID     ReserveUniqueID(const char* str_id);
    ...
};
```

Added PushAllowDuplicateID / PopAllowDuplicateID
------------------------------------------------

imgui_internal.h  / near related ID functions (SetActiveID, GetIDWithSeed, ...)
```cpp
    ...
    IMGUI_API void          PushAllowDuplicateID();         // Disables the tooltip warning when duplicate IDs are detected.
    IMGUI_API void          PopAllowDuplicateID();          // Re-enables the tooltip warning when duplicate IDs are detected.
    ...
```

imgui.cpp:3873 / near ReserveUniqueID() implementation
```cpp
IMGUI_API void ImGui::PushAllowDuplicateID()
{
    ImGuiContext& g = *GImGui;
    g.UidAllowDuplicatesStack++;
}

IMGUI_API void ImGui::PopAllowDuplicateID()
{
    ImGuiContext& g = *GImGui;
    IM_ASSERT(g.UidAllowDuplicatesStack > 0 && "Mismatched PopAllowDuplicateID()");
    g.UidAllowDuplicatesStack--;
}
```

> _Note: doing this via ItemFlags was not feasible as many items (e.g. Button, Checkbox) do not expose flags_

Main logic: ImGui::EndFrame(), NewFrame() and ImGuiWindow::ReserveUniqueID()
----------------------------------------------------------------------------

### ImGui::NewFrame():

```cpp
    ...
    g.UidsThisFrame.resize(0);  // Empty the list of items, but keep its allocated data
    g.UidWasTipDisplayed = false;
```

### ImGui::EndFrame():
```cpp
    IM_ASSERT(g.UidAllowDuplicatesStack == 0 && "Mismatched Push/PopAllowDuplicateID");
    if (g.UidHighlightedTimestamp != ImGui::GetFrameCount())
    {
        g.UidHighlightedTimestamp = 0;
        g.UidHighlightedDuplicate = 0;
    }
```

### ImGuiWindow::ReserveUniqueID()
in imgui.cpp, at the end of "[SECTION] ID STACK".
The pseudocode below summarizes its behavior:

```cpp
IMGUI_API ImGuiID ImGuiWindow::ReserveUniqueID(const char* str_id)
{
    ImGuiContext& g = *GImGui;
    ImGuiID id = GetID(str_id);

    // If PushAllowDuplicateID was called, do not check for uniqueness
    if (g.UidAllowDuplicatesStack != 0)
    return id;

    // Visual aid: a red circle in the top-left corner of all widgets that use a duplicate of the id
    if (id == g.UidHighlightedDuplicate)
        Draw Red dot on top left corner

    // Only check for uniqueness if hovered (this avoid a O(n^2) search at each frame)
    if (id == ImGui::GetHoveredID())
    {
        if (g.UidsThisFrame.contains(id) && !g.UidWasTipDisplayed)
        {
            // Prepare highlight on the next frame for widgets that use this ID
            g.UidHighlightedDuplicate = id;
            g.UidHighlightedTimestamp = ImGui::GetFrameCount();
            if (ImGui::BeginTooltip())
            {
                if (! g.DebugItemPickerActive)
                {
                    // Display tooltip showing the duplicate Id as a string
                    ...
                    // Show hints for correction (PushID, ##)
                    ...

                    ImGui::Text("    Press Ctrl-P to break in any of those items    ");
                    if Ctrl-P
                        g.DebugItemPickerActive = true;
                }
                else
                {
                    // Add additional info at the bottom of the ItemPicker tooltip
                    ImGui::Text("Click on one of the widgets which uses a duplicated item");
                }
                ImGui::EndTooltip();
            }
            g.UidWasTipDisplayed = true;
        }
    }
    g.UidsThisFrame.push_back(id);
    return id;
}
```

imgui_widgets.cpp
-----------------

**Use ReserveUniqueID, allow duplicates when needed (TempInputText, BeginMenuEx)**
I went through all the usages of window->GetID() and changed them to ReserveUniqueID when it was possible / desirable.

### Places with one-line changes

In all the places below, the change was extremely simple: I simply replaced `window->GetID` by `window->ReserveUniqueID`.

For example, in ImGui::ButtonEx
```cpp
bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags)
    ...
    const ImGuiID id = window->ReserveUniqueID(label);  // Instead of window->GetID
```

Below is the list of all function where this simple change was applied:
```cpp
    InvisibleButton
    ArrowButtonEx
    GetWindowScrollbarID
    ImageButton  (both versions)
    Checkbox
    RadioButton
    BeginCombo
    DragScalar
    SliderScalar
    VSliderScalar
    InputTextEx
    ColorButton
    CollapsingHeader
    Selectable
    PlotEx
    BeginTabBar
    TextLink
    TreeNode(const char *)
    TreeNodeEx(const char *)
    TreeNodeExV(const char *)
```

### Places where duplicates are needed

#### TempInputText

TempInputText create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets).
It reuses the widget ID, so we allow it temporarily via PushAllowDuplicateID / PopAllowDuplicateID
```cpp
bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags)
{
    ImGui::PushAllowDuplicateID();
    ...
    ImGui::PopAllowDuplicateID();
}

```

#### GetWindowScrollbarID
GetWindowScrollbarID had to be split in two: ReserveWindowScrollbarID will perform the ID reservation, while GetWindowScrollbarID is only a query.
Luckily, this is the only instance where this was required.

#### BeginMenuEx

In BeginMenuEx, we initially call GetID() instead of ReserveUniqueID() because were are not "consuming" this ID:
instead, we are only querying IsPopupOpen().
The ID will be consumed later by calling ImGui::PushID(label) + Selectable("") (which will lead to the same ID)

```cpp
bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled)
{
    const ImGuiID id = window->GetID(label);
    bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None);

    ...

    PushID(label);
    pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, label_size.y));

    ...
}
```

### Places with no changes, but worth mentioning

* TreeNode(const void *): unchanged
`TreeNode(const void* ptr_id)` and `TreeNodeExV(const void *)` are the only instances which call `window->GetID(const void *)`.
As a consequence, they cannot use the unique id mechanism, but I think their usage is rare enough that is not a big concern.

* ImGui::TabBarCalcTabID: unchanged
It still uses window->GetID, to avoid issues when merging with the docking branch.

* BeginChild, BeginPopup and co are unchanged:
  since they use ImGui::Begin()/End() internally), I suspect they might be called multiple times with the same string,
  since it is allowed with ImGui::Begin/End.

imgui_demo.cpp: some minor changes
----------------------------------

I went through the full demo Window to look for instances where a duplicate ID might be used, and corrected them.
Very few changes were required:

* **"Layout/Text Baseline Alignment"**: simple ID fix  (Added ##)
* **"Widgets/Plotting"**: simple id fix (Added ##)
* **"Columns (legacy API)/Borders"**: added ImGui::PushID for each cell
* **"Widgets/Drag and Drop/Drag to reorder items (simple)"**: no change, but a transient duplicate ID may appear during reordering
    (for *one* frame only: see comments in the code)

Demo code in order to test this PR
==================================

Add and call this function in any imgui example. This code will trigger the warnings for duplicated IDs,
and will also enable to make sure that the fixes that had to be done are OK.

```cpp
char text1[500] = "Hello,";
char text2[500] = "world!";
char text3[500] = "Hello,";
char text4[500] = "world!";
int value = 0;
bool flag1 = false, flag2 = false;
float f1 = 0.f, f2 = 0.f, f3 = 0.f, f4 = 0.f;

void Gui()
{
    // Some widgets to show the duplicated ID warnings
    ImGui::SetNextWindowSize(ImVec2(400, 500));
    if (ImGui::Begin("Duplicated ID examples", nullptr, ImGuiWindowFlags_MenuBar))
    {
        ImGui::TextWrapped("Example of widgets using duplicated IDs");
        ImGui::Separator();

        ImGui::TextWrapped("Only the first button is clickable (same ID)");
        if (ImGui::Button("button")) printf("Button1 pressed\n");
        if (ImGui::Button("button")) printf("Button2 pressed\n");

        ImGui::Separator();

        ImGui::TextWrapped("Those text inputs will share the same value (editing one will overwrite the other)");
        ImGui::InputText("text", text1, IM_ARRAYSIZE(text1));
        ImGui::InputText("text", text2, IM_ARRAYSIZE(text2));

        ImGui::Separator();

        ImGui::TextWrapped("Those radio buttons will share the same value (selecting one will overwrite the other)");
        ImGui::RadioButton("radio", &value, 0);
        ImGui::RadioButton("radio", &value, 1);

        ImGui::Separator();

        ImGui::TextWrapped("The second checkbox cannot be toggled");
        ImGui::Checkbox("checkbox", &flag1);
        ImGui::Checkbox("checkbox", &flag2);

        ImGui::Separator();

        ImGui::TextWrapped("Those two sliders will share the same value (editing one will overwrite the other");
        ImGui::SliderFloat("slider", &f1, 0.f, 1.f);
        ImGui::SliderFloat("slider", &f2, 0.f, 1.f);

        ImGui::Separator();
    }
    ImGui::End();

    // Some widgets to test the fixes that had to be done
    ImGui::SetNextWindowSize(ImVec2(400, 400));
    if (ImGui::Begin("Regression tests", nullptr, ImGuiWindowFlags_MenuBar))
    {
        ImGui::TextWrapped("Small issues had to be fixed inside DragFloat (cf TempInputText), InputTextMultiline and BeginMenuBar.\n"
            "This window is here to test those fixes.");

        ImGui::Separator();

        if (ImGui::BeginMenuBar())
        {
            if (ImGui::BeginMenu("My Menu"))
            {
                ImGui::MenuItem("Item 1");
                ImGui::EndMenu();
            }
            ImGui::EndMenuBar();
        }

        ImGui::TextWrapped("DragFloat will reuse the Item ID when double clicking. This is allowed via Push/PopAllowDuplicateID");
        ImGui::DragFloat("dragf", &f3);

        ImGui::Separator();

        ImGui::TextWrapped("An issue was fixed with the InputTextMultiline vertical scrollbar, which trigerred a duplicate ID warning, which was fixed");
        ImGui::InputTextMultiline("multiline", text1, 500);

    }
    ImGui::End();

    // A window that is populated in several steps should still detect duplicates
    {
        ImGui::SetNextWindowSize(ImVec2(400, 200));

        ImGui::Begin("Window populated in several steps");
        ImGui::TextWrapped("This window is populated between several ImGui::Begin/ImGui::End calls (with the same window name).\n"
                           "Duplicates should be detected anyhow.");
        ImGui::Text("A first InputText");
        ImGui::InputText("TTT", text3, 500);
        ImGui::End();

        ImGui::Begin("Window populated in several steps");
        ImGui::Separator();
        ImGui::Text("The second InputText below reuses the same ID, \nin a different ImGui::Begin/ImGui::End context \n(but with the same window name)");
        ImGui::InputText("TTT", text4, 500);
        ImGui::End();
    }
}
```
@ocornut
Copy link
Owner

ocornut commented Sep 10, 2024

Moved to #7961 and solved now. Thanks for the push!

@ocornut ocornut closed this as completed Sep 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
label/id and id stack implicit identifiers, pushid(), id stack
Projects
None yet
Development

No branches or pull requests

3 participants