ocornut / imgui

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

Background color for 'ImGui::Selectable()' #4719

Open Nightfallen opened 2 years ago

Nightfallen commented 2 years ago

Version/Branch of Dear ImGui:

Version: 1.86 WIP (18510) Branch: docking

Back-end/Renderer/Compiler/OS

Back-ends: imgui_impl_dx11.cpp + imgui_impl_win32.cpp Compiler: MSVC Operating System: Windows 10

Info from 'Demo>About Window'
``` Dear ImGui 1.86 WIP (18510) -------------------------------- sizeof(size_t): 8, sizeof(ImDrawIdx): 2, sizeof(ImDrawVert): 20 define: __cplusplus=199711 define: _WIN32 define: _WIN64 define: _MSC_VER=1931 define: _MSVC_LANG=202004 define: IMGUI_HAS_VIEWPORT define: IMGUI_HAS_DOCK -------------------------------- io.BackendPlatformName: imgui_impl_win32 io.BackendRendererName: imgui_impl_dx11 io.ConfigFlags: 0x00000401 NavEnableKeyboard ViewportsEnable io.ConfigViewportsNoAutoMerge io.ConfigViewportsNoDecoration io.ConfigInputTextCursorBlink io.ConfigWindowsResizeFromEdges io.ConfigWindowsMoveFromTitleBarOnly io.ConfigMemoryCompactTimer = 60.0 io.BackendFlags: 0x00001C0E HasMouseCursors HasSetMousePos PlatformHasViewports HasMouseHoveredViewport RendererHasVtxOffset RendererHasViewports -------------------------------- io.Fonts: 1 fonts, Flags: 0x00000000, TexSize: 512,64 io.DisplaySize: 50.00,50.00 io.DisplayFramebufferScale: 1.00,1.00 -------------------------------- style.WindowPadding: 8.00,8.00 style.WindowBorderSize: 1.00 style.FramePadding: 4.00,3.00 style.FrameRounding: 0.00 style.FrameBorderSize: 0.00 style.ItemSpacing: 8.00,4.00 style.ItemInnerSpacing: 4.00,4.00 ```

My Issue/Question: How to make something like background color for 'ImGui::Selectable()'

I want to make background color for 'ImGui::Selectable()' when it's not hovered, held or selected (for which there are ImGuiCol_Header colors)

I found i can render rects, but i have troubles with them. In first case Selectable's text rendered behind rect. In second case i can't use 'ImGui::IsItemHovered()' Although there is 1-2 extra pixels at the top and bottom of rect, I'm not sure why

I want to get some help or ideas how could i implement it

Screenshots/Video

screen pixel

Standalone, minimal, complete and verifiable example:

Code snippet
``` void SelectableColor(ImVec2 szSelectable, ImU32 color) { auto& style = ImGui::GetStyle(); ImVec2 itemSpacing = style.ItemSpacing; ImVec2 p_min = ImGui::GetCursorScreenPos() - style.FramePadding; ImVec2 p_max = p_min + itemSpacing + szSelectable; ImGui::GetWindowDrawList()->AddRectFilled(p_min, p_max, color); } void SelectableColorEx(ImVec2 prevCurPos, ImVec2 szSelectable, ImU32 color) { auto& style = ImGui::GetStyle(); ImVec2 itemSpacing = style.ItemSpacing; ImVec2 p_min = prevCurPos - style.FramePadding; ImVec2 p_max = p_min + itemSpacing + szSelectable; ImGui::GetWindowDrawList()->AddRectFilled(p_min, p_max, color); } void MyImguiWindow(bool* is_open) { ImGui::Begin("##Temp Window", is_open); constexpr int szArr = 9; static bool selectables[szArr] = {}; ImVec2 szEl = ImVec2(40, 40); auto flags = ImGuiSelectableFlags_SelectOnClick; // Text of selectable is behind rect bool separator = false; for (int i = 0; i < 9; ++i) { if (separator) ImGui::SameLine(); std::string label = std::to_string(i + 1); auto prevCurPos = ImGui::GetCursorScreenPos(); ImGui::Selectable(label.data(), &selectables[i], flags, szEl); if (!ImGui::IsItemHovered() && !selectables[i]) { SelectableColorEx(prevCurPos, szEl, IM_COL32(255, 0, 0, 200)); } separator = true; } // Text of selectable is above rect, but i can't use 'ImGui::IsItemHovered()' separator = false; for (int i = 0; i < 9; ++i) { if (separator) ImGui::SameLine(); std::string label = std::to_string(i + 1); auto prevCurPos = ImGui::GetCursorScreenPos(); if (!ImGui::IsItemHovered() && !selectables[i]) { SelectableColor(szEl, IM_COL32(255, 0, 0, 200)); } ImGui::Selectable(label.data(), &selectables[i], flags, szEl); separator = true; } ImGui::End(); } ```
rokups commented 2 years ago

You are on the right track. Until a style color exits you should be rendering a background yourself through a drawlist and there is a much easier way to fix all your problems. You can use GetItemRectMin()/GetItemRectMax() to get a rect of last rendered item. This will save you a headache of correctly recalculating item size as it is not always obvious what style parameters go into the calculation. Then you should use a drawlist channel splitter to change rendering order and draw background behind Selectable() even though in the code Selectable() comes first (it has to, in order to use GetItemRectMin()/GetItemRectMax() ). Here is a fixed version of your code doing these things:

void SelectableColor(ImU32 color)
{
    ImVec2 p_min = ImGui::GetItemRectMin();
    ImVec2 p_max = ImGui::GetItemRectMax();
    ImGui::GetWindowDrawList()->AddRectFilled(p_min, p_max, color);
}

void MyImguiWindow(bool* is_open)
{
    ImGui::Begin("##Temp Window", is_open);
    constexpr int szArr = 9;
    static bool selectables[szArr] = {};
    ImVec2 szEl = ImVec2(40, 40);
    auto flags = ImGuiSelectableFlags_SelectOnClick;
    ImDrawList* draw_list = ImGui::GetWindowDrawList();

    // Text of selectable is behind rect
    bool separator = false;
    for (int i = 0; i < 9; ++i)
    {
        if (separator) ImGui::SameLine();
        std::string label = std::to_string(i + 1);

        // The Magick.
        draw_list->ChannelsSplit(2);

        // Channel number is like z-order. Widgets in higher channels are rendered above widgets in lower channels.
        draw_list->ChannelsSetCurrent(1);
        ImGui::Selectable(label.data(), &selectables[i], flags, szEl);

        if (!ImGui::IsItemHovered() && !selectables[i])
        {
            // Render background behind Selectable().
            draw_list->ChannelsSetCurrent(0);
            SelectableColor(IM_COL32(255, 0, 0, 200));
        }

        // And commit changes.
        draw_list->ChannelsMerge();
        separator = true;
    }

    ImGui::End();
}

A small advice: use of std::string may allocate memory, best not create std::string in a rendering loop as you may end up allocating and freeing memory constantly if you are not extremely careful.

Nightfallen commented 2 years ago

Thank you, now it's working fine Screenshot 2021-11-15 144233

ocornut commented 2 years ago

It may be simpler and faster to just always mark the item as Selected and change the color used for Selection?

Nightfallen commented 2 years ago

I think no, i have selectables matrix and i need their actual states. With the code above i can make something like this and just change calls to ImGui::Selectable() to CustomSelectable() without changing logic of items being selected based on colors:

bool CustomSelectable(const char* label, bool* p_selected, ImU32 bg_color, ImGuiSelectableFlags flags, const ImVec2& size_arg)
{
ImDrawList* draw_list = ImGui::GetWindowDrawList();
draw_list->ChannelsSplit(2);

// Channel number is like z-order. Widgets in higher channels are rendered above widgets in lower channels.
draw_list->ChannelsSetCurrent(1);

bool result = ImGui::Selectable(label, p_selected, flags, size_arg);

if (!ImGui::IsItemHovered() && !ImGui::IsItemActive() && !*p_selected)
{
    // Render background behind Selectable().
    draw_list->ChannelsSetCurrent(0);
    SelectableColor(bg_color);
}

// Commit changes.
draw_list->ChannelsMerge();
return result;
}

And now i have something like this: range