imgui icon indicating copy to clipboard operation
imgui copied to clipboard

Using BeginCombo() and avanced: custom preview, filtering

Open ocornut opened this issue 7 years ago • 40 comments

I feel like this is one of the small/nice feature that may have passed under the radar so I'm going to write a small blurb about it here.

There's a BeginCombo/EndCombo() api which is much more flexible that the "old" Combo() function.

bool BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags = 0);
void EndCombo(); // only call EndCombo() if BeginCombo() returns true!

Basically with this api you can control the way you store your current selection and item storage (they don't have to be stored sequentially and randomly accessible, so if your natural "selection" data is a pointer you can use that, you can submit filtered lists easily, etc.

const char* items[] = { "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIII", "JJJJ", "KKKK", "LLLLLLL", "MMMM", "OOOOOOO", "PPPP", "QQQQQQQQQQ", "RRR", "SSSS" };
static const char* current_item = NULL;

if (ImGui::BeginCombo("##combo", current_item)) // The second parameter is the label previewed before opening the combo.
{
    for (int n = 0; n < IM_ARRAYSIZE(items); n++)
    {
        bool is_selected = (current_item == items[n]); // You can store your selection however you want, outside or inside your objects
        if (ImGui::Selectable(items[n], is_selected)
            current_item = items[n];
        if (is_selected)
            ImGui::SetItemDefaultFocus();   // You may set the initial focus when opening the combo (scrolling + for keyboard navigation support)
    }
    ImGui::EndCombo();
}

You can easily build combo boxes for your custom types using this.

Today I asked extra flags: ImGuiComboFlags_NoArrowButton

image

ImGuiComboFlags_NoPreview

image

You could previously achieve this by pushing an item width the width of the button only, but it's not doable with just a flag.

ImGuiComboFlags_NoPreview + hidden label:

image

Also consider creating custom layout like:

const char* items[] = { "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIII", "JJJJ", "KKKK", "LLLLLLL", "MMMM", "OOOOOOO", "PPPP", "QQQQQQQQQQ", "RRR", "SSSS" };
static const char* current_item = NULL;
ImGuiComboFlags flags = ImGuiComboFlags_NoArrowButton;

ImGuiStyle& style = ImGui::GetStyle();
float w = ImGui::CalcItemWidth();
float spacing = style.ItemInnerSpacing.x;
float button_sz = ImGui::GetFrameHeight();
ImGui::PushItemWidth(w - spacing * 2.0f - button_sz * 2.0f);
if (ImGui::BeginCombo("##custom combo", current_item, ImGuiComboFlags_NoArrowButton))
{
    for (int n = 0; n < IM_ARRAYSIZE(items); n++)
    {
        bool is_selected = (current_item == items[n]);
        if (ImGui::Selectable(items[n], is_selected))
            current_item = items[n];
        if (is_selected)
            ImGui::SetItemDefaultFocus();
    }
    ImGui::EndCombo();
}
ImGui::PopItemWidth();
ImGui::SameLine(0, spacing);
if (ImGui::ArrowButton("##r", ImGuiDir_Left))
{
}
ImGui::SameLine(0, spacing);
if (ImGui::ArrowButton("##r", ImGuiDir_Right))
{
}
ImGui::SameLine(0, style.ItemInnerSpacing.x);
ImGui::Text("Custom Combo");

image

Clicking the preview area will get you the normal combo popup, etc.

ocornut avatar Mar 03 '18 19:03 ocornut

Hi,

Nice example! Is it possible to add an image before the label in the selection list, or in the selected choice?

jdumas avatar Mar 15 '18 23:03 jdumas

Yes, they are regular popup so you can do anything within them.

ocornut avatar Mar 15 '18 23:03 ocornut

I managed to display an image per item by calling a ImGui::Selectable("", is_selected);, followed by ImGui::SameLine(); and then showing image + text:

cmap

However, I cannot figure out how to show the picture of the colormap inside the frame that shows the selected one.

EDIT: Well ok what I did doesn't really work actually. If I set the selectable size to "" then I cannot click on the image or the text to change the selected value ... I'm sure it's possible to fix this with the current API, but I haven't figure out yet how.

jdumas avatar Mar 15 '18 23:03 jdumas

EDIT: Well ok what I did doesn't really work actually. If I set the selectable size to "" then I cannot click on the image or the text to change the selected value ... I'm sure it's possible to fix this with the current API, but I haven't figure out yet how.

If you set the selectable label to "" make sure there is a PushID() or use "##someid" else all your selectable are using the same identifier and will conflict. Selectable("") + SameLine + Image + SameLine + Text should work.

However, I cannot figure out how to show the picture of the colormap inside the frame that shows the selected one.

That's a trickier one unfortunately, thought you can draw inside the combo widget:

ImVec2 combo_pos = ImGui::GetCursorScreenPos();
if (ImGui::BeginCombo(label, ""))
{
     [...]
    ImGui::EndCombo();
}
[...]
ImVec2 backup_pos = ImGui::GetCursorScreenPos();
ImGuiStyle& style = ImGui::GetStyle();
ImGui::SetCursorScreenPos(ImVec2(combo_pos.x + style.FramePadding.x, combo_pos.y));
ImGui::ColorButton("blah", ImVec4(1,0,0,1));
ImGui::SameLine();
ImGui::Text("Hello");
ImGui::SetCursorScreenPos(backup_pos);

image

But you will run into clipping issues (only you push a clipping rectangle or use internal functions that draw clipped text). Maybe this pattern could be formalized into something better.

I think you would be better off using Image + SameLine + Combo with a manipulation of item width.

ocornut avatar Mar 16 '18 14:03 ocornut

Right now my window has a fixed size, and the text fit in the box, so what you suggest may just work (™). Thanks for the tip! It works indeed better with the PushID() :p

While I'm at it, what is the correct way to calculate the size of the button? I am using ImGui::GetStyle().FrameRounding = 2.0f; and ImGui::GetStyle().FrameBorderSize = 1.0f;, and I am positioning my text and positions as so:

ImGui::SetCursorScreenPos(ImVec2(combo_pos.x + style.FramePadding.x, combo_pos.y + style.FramePadding.y));
float h = ImGui::GetTextLineHeightWithSpacing() - style.FramePadding.y;
ImGui::Image(tex_id, ImVec2(h, h);
ImGui::SameLine();
ImGui::Text("Viridis");

padding

The image seems a bit too big and the padding is not even on top and bottom, so I may have been doing the calculation wrong.

jdumas avatar Mar 16 '18 15:03 jdumas

While I'm at it, what is the correct way to calculate the size of the button?

Neither FrameRounding or FrameBorderSize after the size of elements (the border size is taken "inside" the item). The size of a button is typically size of text + FramePadding * 2.

float h = ImGui::GetTextLineHeightWithSpacing() - style.FramePadding.y;

Here you probably want to just float h = ImGui::GetTextLineHeight().

ocornut avatar Mar 16 '18 15:03 ocornut

You are right of course =). GetTextLineHeight() works just fine here. Thanks!

jdumas avatar Mar 16 '18 15:03 jdumas

Hey,

I am browsing closed issues and cannot find any reference to create a filtered combo.

What's the best current option to create a FilterCombo widget as seen in UE4/Sublime/etc? Ie, some kind of Input filter on top then a filtered combo list below.

Ty!

PS: The pic below uses fuzzy pattern matching rather than simple filtering, but you get the idea.

image

r-lyeh avatar Oct 05 '18 09:10 r-lyeh

I don't have a good answer for you, for not having tried to make an interactive one, but #718 is the thread to check to fish for ideas.

ocornut avatar Oct 05 '18 11:10 ocornut

Ah kewl :)

I've started from your snippet in #718 and started to mess with it to add interactivity + basic fuzzy search. It could be 1,000 times better but works for me now :D

// imgui combo filter v1.0, by @r-lyeh (public domain)
// contains code by @harold-b (public domain?)

/*  Demo: */
/*
    {
        // requisite: hints must be alphabetically sorted beforehand
        const char *hints[] = {
            "AnimGraphNode_CopyBone",
            "ce skipaa",
            "ce skipscreen",
            "ce skipsplash",
            "ce skipsplashscreen",
            "client_unit.cpp",
            "letrograd",
            "level",
            "leveler",
            "MacroCallback.cpp",
            "Miskatonic university",
            "MockAI.h",
            "MockGameplayTasks.h",
            "MovieSceneColorTrack.cpp",
            "r.maxfps",
            "r.maxsteadyfps",
            "reboot",
            "rescale",
            "reset",
            "resource",
            "restart",
            "retrocomputer",
            "retrograd",
            "return",
            "slomo 10",
            "SVisualLoggerLogsList.h",
            "The Black Knight",
        };
        static ComboFilterState s = {0};
        static char buf[128] = "type text here...";
        if( ComboFilter("my combofilter", buf, IM_ARRAYSIZE(buf), hints, IM_ARRAYSIZE(hints), s) ) {
            puts( buf );
        }
    }
*/

#pragma once

struct ComboFilterState
{
    int  activeIdx;         // Index of currently 'active' item by use of up/down keys
    bool selectionChanged;  // Flag to help focus the correct item when selecting active item
};

static bool ComboFilter__DrawPopup( ComboFilterState& state, int START, const char **ENTRIES, int ENTRY_COUNT )
{
    using namespace ImGui;
    bool clicked = 0;

    // Grab the position for the popup
    ImVec2 pos = GetItemRectMin(); pos.y += GetItemRectSize().y;
    ImVec2 size = ImVec2( GetItemRectSize().x-60, GetItemsLineHeightWithSpacing() * 4 );

    PushStyleVar( ImGuiStyleVar_WindowRounding, 0 );

    ImGuiWindowFlags flags = 
        ImGuiWindowFlags_NoTitleBar          | 
        ImGuiWindowFlags_NoResize            |
        ImGuiWindowFlags_NoMove              |
        ImGuiWindowFlags_HorizontalScrollbar |
        ImGuiWindowFlags_NoSavedSettings     |
        0; //ImGuiWindowFlags_ShowBorders;

    SetNextWindowFocus();

    SetNextWindowPos ( pos );
    SetNextWindowSize( size );
    Begin("##combo_filter", nullptr, flags );

    PushAllowKeyboardFocus( false );

    for( int i = 0; i < ENTRY_COUNT; i++ ) {
        // Track if we're drawing the active index so we
        // can scroll to it if it has changed
        bool isIndexActive = state.activeIdx == i;

        if( isIndexActive ) {
            // Draw the currently 'active' item differently
            // ( used appropriate colors for your own style )
            PushStyleColor( ImGuiCol_Border, ImVec4( 1, 1, 0, 1 ) );
        }

        PushID( i );
        if( Selectable( ENTRIES[i], isIndexActive ) ) {
            // And item was clicked, notify the input
            // callback so that it can modify the input buffer
            state.activeIdx = i;
            clicked = 1;
        }
        if( IsItemFocused() && IsKeyPressed(GetIO().KeyMap[ImGuiKey_Enter]) ) {
            // Allow ENTER key to select current highlighted item (w/ keyboard navigation)
            state.activeIdx = i;
            clicked = 1;
        }
        PopID();

        if( isIndexActive ) {
            if( state.selectionChanged ) {
                // Make sure we bring the currently 'active' item into view.
                SetScrollHere();
                state.selectionChanged = false;
            }

            PopStyleColor(1);
        }
    }

    PopAllowKeyboardFocus();
    End();
    PopStyleVar(1);

    return clicked;
}

static bool ComboFilter( const char *id, char *buffer, int bufferlen, const char **hints, int num_hints, ComboFilterState &s ) {
    struct fuzzy {
        static int score( const char *str1, const char *str2 ) {
            int score = 0, consecutive = 0, maxerrors = 0;
            while( *str1 && *str2 ) {
                int is_leading = (*str1 & 64) && !(str1[1] & 64);
                if( (*str1 & ~32) == (*str2 & ~32) ) {
                    int had_separator = (str1[-1] <= 32);
                    int x = had_separator || is_leading ? 10 : consecutive * 5;
                    consecutive = 1;
                    score += x;
                    ++str2;
                } else {
                    int x = -1, y = is_leading * -3;
                    consecutive = 0;
                    score += x;
                    maxerrors += y;
                }
                ++str1;
            }
            return score + (maxerrors < -9 ? -9 : maxerrors);
        }
        static int search( const char *str, int num, const char *words[] ) {
            int scoremax = 0;
            int best = -1;
            for( int i = 0; i < num; ++i ) {
                int score = fuzzy::score( words[i], str );
                int record = ( score >= scoremax );
                int draw = ( score == scoremax );
                if( record ) {
                    scoremax = score;
                    if( !draw ) best = i;
                    else best = best >= 0 && strlen(words[best]) < strlen(words[i]) ? best : i;
                }
            }
            return best;
        }
    };
    using namespace ImGui;
    bool done = InputText(id, buffer, bufferlen, ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_EnterReturnsTrue );
    bool hot = s.activeIdx >= 0 && strcmp(buffer, hints[s.activeIdx]);
    if( hot ) {
        int new_idx = fuzzy::search( buffer, num_hints, hints );
        int idx = new_idx >= 0 ? new_idx : s.activeIdx;
        s.selectionChanged = s.activeIdx != idx;
        s.activeIdx = idx;
        if( done || ComboFilter__DrawPopup( s, idx, hints, num_hints ) ) {
            int i = s.activeIdx;
            if( i >= 0 ) {
                strcpy(buffer, hints[i]);
                done = true;
            }
        }
    }
    return done;
}

PS: sorry no gifs today!

EDIT: updated code to v1.0

r-lyeh avatar Oct 05 '18 16:10 r-lyeh

I've updated the snippet above to a cleaner implementation. Also, here's latest gif anim (showcasing mouse & keyboardnav picking):

gif

r-lyeh avatar Oct 15 '18 12:10 r-lyeh

Thanks @r-lyeh for the code and gif, this is really nice!

ocornut avatar Oct 16 '18 08:10 ocornut

I just stumbled upon how FlatUI handles the combo filtering, and its logic fits better with IMGUI mentality IMO.

I will try to put the filter inside the combo popup (and leave the header to behave exactly like current Combo). Will create a new snippet someday.

image

flat-ui demo: http://designmodo.github.io/Flat-UI/

r-lyeh avatar Oct 17 '18 13:10 r-lyeh

Hello guys, check out my solution over this problem. I have taken approach @r-lyeh about fuzzy search and tried to create total combo-like widget

/*  Demo: */
/*
const char *hints[] = {
                "AnimGraphNode_CopyBone",
                "ce skipaa",
                "ce skipscreen",
                "ce skipsplash",
                "ce skipsplashscreen",
                "client_unit.cpp",
                "letrograd",
                "level",
                "leveler",
                "MacroCallback.cpp",
                "Miskatonic university",
                "MockAI.h",
                "MockGameplayTasks.h",
                "MovieSceneColorTrack.cpp",
                "r.maxfps",
                "r.maxsteadyfps",
                "reboot",
                "rescale",
                "reset",
                "resource",
                "restart",
                "retrocomputer",
                "retrograd",
                "return",
                "slomo 10",
                "SVisualLoggerLogsList.h",
                "The Black Knight",
            };
            static ComboFilterState s = {0, false};
            static char buf[128];
            static bool once = false;
            if(!once) {
                memcpy(buf, hints[0], strlen(hints[0]) + 1);
                once = true;
            }
            
            if( ComboFilter("my combofilter", buf, IM_ARRAYSIZE(buf), hints, IM_ARRAYSIZE(hints), s) ) {
                //...picking was occured
            }
*/

#pragma once

struct ComboFilterState {
    int  activeIdx;
    bool selectionChanged;
};

bool ComboFilter(const char *label, char *buffer, int bufferlen, const char **hints, int num_hints, ComboFilterState &s, ImGuiComboFlags flags = 0) {
    
    using namespace ImGui;
        
    s.selectionChanged = false;
    
    // Always consume the SetNextWindowSizeConstraint() call in our early return paths
    ImGuiContext& g = *GImGui;

    ImGuiWindow* window = GetCurrentWindow();
    if (window->SkipItems)
        return false;
    
    const ImGuiID id = window->GetID(label);
    bool popup_open = IsPopupOpen(id);
    bool popupNeedBeOpen = strcmp(buffer, hints[s.activeIdx]);
    bool popupJustOpened = false;
    
    IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together

    const ImGuiStyle& style = g.Style;
    
    const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
    const ImVec2 label_size = CalcTextSize(label, NULL, true);
    const float expected_w = CalcItemWidth();
    const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : expected_w;
    const ImRect frame_bb(window->DC.CursorPos, ImVec2(window->DC.CursorPos.x + w, window->DC.CursorPos.y + label_size.y + style.FramePadding.y*2.0f));
    const ImRect total_bb(frame_bb.Min, ImVec2((label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f) + frame_bb.Max.x, frame_bb.Max.y));
    const float value_x2 = ImMax(frame_bb.Min.x, frame_bb.Max.x - arrow_size);
    ItemSize(total_bb, style.FramePadding.y);
    if (!ItemAdd(total_bb, id, &frame_bb))
        return false;
    
    
    bool hovered, held;
    bool pressed = ButtonBehavior(frame_bb, id, &hovered, &held);
    
    if(!popup_open) {
        const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
        RenderNavHighlight(frame_bb, id);
        if (!(flags & ImGuiComboFlags_NoPreview))
            window->DrawList->AddRectFilled(frame_bb.Min, ImVec2(value_x2, frame_bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawCornerFlags_All : ImDrawCornerFlags_Left);
    }
    if (!(flags & ImGuiComboFlags_NoArrowButton))
    {
        ImU32 bg_col = GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
        ImU32 text_col = GetColorU32(ImGuiCol_Text);
        window->DrawList->AddRectFilled(ImVec2(value_x2, frame_bb.Min.y), frame_bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawCornerFlags_All : ImDrawCornerFlags_Right);
        if (value_x2 + arrow_size - style.FramePadding.x <= frame_bb.Max.x)
            RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, frame_bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f);
    }
    if(!popup_open) {

        RenderFrameBorder(frame_bb.Min, frame_bb.Max, style.FrameRounding);
        if (buffer != NULL && !(flags & ImGuiComboFlags_NoPreview))
            
            RenderTextClipped(ImVec2(frame_bb.Min.x + style.FramePadding.x, frame_bb.Min.y + style.FramePadding.y), ImVec2(value_x2, frame_bb.Max.y), buffer, NULL, NULL, ImVec2(0.0f,0.0f));

        if ((pressed || g.NavActivateId == id || popupNeedBeOpen) && !popup_open)
        {
            if (window->DC.NavLayerCurrent == 0)
                window->NavLastIds[0] = id;
            OpenPopupEx(id);
            popup_open = true;
            popupJustOpened = true;
        }
    }
    
    if (label_size.x > 0)
    RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
    
    if (!popup_open) {
        return false;
    }
    
    const float totalWMinusArrow = w - arrow_size;
    struct ImGuiSizeCallbackWrapper {
        static void sizeCallback(ImGuiSizeCallbackData* data)
        {
            float* totalWMinusArrow = (float*)(data->UserData);
            data->DesiredSize = ImVec2(*totalWMinusArrow, 200.f);
        }
    };
    SetNextWindowSizeConstraints(ImVec2(0 ,0), ImVec2(totalWMinusArrow, 150.f), ImGuiSizeCallbackWrapper::sizeCallback, (void*)&totalWMinusArrow);

    char name[16];
    ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.BeginPopupStack.Size); // Recycle windows based on depth

    // Peak into expected window size so we can position it
    if (ImGuiWindow* popup_window = FindWindowByName(name))
        if (popup_window->WasActive)
        {
            ImVec2 size_expected = CalcWindowExpectedSize(popup_window);
            if (flags & ImGuiComboFlags_PopupAlignLeft)
                popup_window->AutoPosLastDirection = ImGuiDir_Left;
            ImRect r_outer = GetWindowAllowedExtentRect(popup_window);
            ImVec2 pos = FindBestWindowPosForPopupEx(frame_bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, frame_bb, ImGuiPopupPositionPolicy_ComboBox);
            
            pos.y -= label_size.y + style.FramePadding.y*2.0f;
            
            SetNextWindowPos(pos);
        }

    // Horizontally align ourselves with the framed text
    ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings;
//    PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(style.FramePadding.x, style.WindowPadding.y));
    bool ret = Begin(name, NULL, window_flags);

    ImGui::PushItemWidth(ImGui::GetWindowWidth());
    ImGui::SetCursorPos(ImVec2(0.f, window->DC.CurrLineTextBaseOffset));
    if(popupJustOpened) {
        ImGui::SetKeyboardFocusHere(0);
    }
    bool done = InputTextEx("", NULL, buffer, bufferlen, ImVec2(0, 0), ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_EnterReturnsTrue, NULL, NULL);
    ImGui::PopItemWidth();

    if(s.activeIdx < 0) {
        IM_ASSERT(false); //Undefined behaviour
        return false;
    }
    
    
    if (!ret)
    {
        ImGui::EndChild();
        ImGui::PopItemWidth();
        EndPopup();
        IM_ASSERT(0);   // This should never happen as we tested for IsPopupOpen() above
        return false;
    }
    
    
    ImGuiWindowFlags window_flags2 =  0; //ImGuiWindowFlags_HorizontalScrollbar
    ImGui::BeginChild("ChildL", ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y), false, window_flags2);
    
    
    
    
    struct fuzzy {
        static int score( const char *str1, const char *str2 ) {
            int score = 0, consecutive = 0, maxerrors = 0;
            while( *str1 && *str2 ) {
                int is_leading = (*str1 & 64) && !(str1[1] & 64);
                if( (*str1 & ~32) == (*str2 & ~32) ) {
                    int had_separator = (str1[-1] <= 32);
                    int x = had_separator || is_leading ? 10 : consecutive * 5;
                    consecutive = 1;
                    score += x;
                    ++str2;
                } else {
                    int x = -1, y = is_leading * -3;
                    consecutive = 0;
                    score += x;
                    maxerrors += y;
                }
                ++str1;
            }
            return score + (maxerrors < -9 ? -9 : maxerrors);
        }
        static int search( const char *str, int num, const char *words[] ) {
            int scoremax = 0;
            int best = -1;
            for( int i = 0; i < num; ++i ) {
                int score = fuzzy::score( words[i], str );
                int record = ( score >= scoremax );
                int draw = ( score == scoremax );
                if( record ) {
                    scoremax = score;
                    if( !draw ) best = i;
                    else best = best >= 0 && strlen(words[best]) < strlen(words[i]) ? best : i;
                }
            }
            return best;
        }
    };
    
    int new_idx = fuzzy::search( buffer, num_hints, hints );
    int idx = new_idx >= 0 ? new_idx : s.activeIdx;
    s.selectionChanged = s.activeIdx != idx;
    bool selectionChangedLocal = s.selectionChanged;
    s.activeIdx = idx;
    
    if(done) {
        CloseCurrentPopup();
    }
    for (int n = 0; n < num_hints; n++) {;
        bool is_selected = n == s.activeIdx;
        if (is_selected && (IsWindowAppearing() || selectionChangedLocal)) {
             SetScrollHereY();
//            ImGui::SetItemDefaultFocus();
        }
        if (ImGui::Selectable(hints[n], is_selected)) {
            s.selectionChanged = s.activeIdx != n;
            s.activeIdx = n;
            strcpy(buffer, hints[n]);
            CloseCurrentPopup();
        }
    }
    ImGui::EndChild();
    EndPopup();

    return s.selectionChanged && !strcmp(hints[s.activeIdx], buffer);
}

Keep in mind that you need to keep hints list sorted

ezgif com-video-to-gif

kovewnikov avatar Mar 16 '20 09:03 kovewnikov

missing a pic!

r-lyeh avatar Mar 16 '20 11:03 r-lyeh

missing a pic!

Done

kovewnikov avatar Mar 17 '20 17:03 kovewnikov

Using this thread and drawImage seems to work, thanks all!

notes

  float button_sz = ImGui::GetFrameHeight();
                        if(ImGui::BeginCombo("##custom combo",current_item,ImGuiComboFlags_NoArrowButton))
                        {
                            for(int n = 0; n < IM_ARRAYSIZE(items); n++)
                            {
                                bool is_selected = (current_item == items[n]);
                                auto drawList = ImGui::GetWindowDrawList();
                                if(ImGui::Selectable(items[n],is_selected))
                                {
                                    selected_item_index = n;
                                    current_item = items[n];
                                }
                                auto rect_min = ImGui::GetItemRectMin();
                                auto rect_max = ImGui::GetItemRectMax();
                                rect_max.x = rect_min.x + 32;
                                drawList->AddImage(ImGui_GetOpenGLTexture(s_noteIcons[n]),rect_min,rect_max,ImVec2(0,0),ImVec2(1,1),IM_COL32(255,255,255,255));
                                if(is_selected)
                                    ImGui::SetItemDefaultFocus();
                            }
                            ImGui::EndCombo();
                        }
                        else
                        {
                            auto rect_min = ImGui::GetItemRectMin();
                            auto rect_max = ImGui::GetItemRectMax();
                            rect_max.x = rect_min.x + 32;
                            auto drawList = ImGui::GetWindowDrawList();
                            drawList->AddImage(ImGui_GetOpenGLTexture(s_noteIcons[selected_item_index]),rect_min,rect_max,ImVec2(0,0),ImVec2(1,1),IM_COL32(255,255,255,255));
                        }

erwincoumans avatar Jan 31 '21 23:01 erwincoumans

const char* items[] = { "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIII", "JJJJ", "KKKK", "LLLLLLL", "MMMM", "OOOOOOO", "PPPP", "QQQQQQQQQQ", "RRR", "SSSS" };
static const char* current_item = NULL;

if (ImGui::BeginCombo("##combo", current_item)) // The second parameter is the label previewed before opening the combo.
{
    for (int n = 0; n < IM_ARRAYSIZE(items); n++)
    {
        bool is_selected = (current_item == items[n]); // You can store your selection however you want, outside or inside your objects
        if (ImGui::Selectable(items[n], is_selected)
            current_item = items[n];
        if (is_selected)
            ImGui::SetItemDefaultFocus();   // You may set the initial focus when opening the combo (scrolling + for keyboard navigation support)
    }
    ImGui::EndCombo();
}

Just letting you know. You're missing a ) at if (ImGui::Selectable(items[n], is_selected)

Nuxar1 avatar Feb 07 '21 13:02 Nuxar1

On the topic of customizing the combo preview

I have pushed an experimental API (declared in imgui_internal.h)

if (ImGui::BeginCombo("combo custom", "", flags | ImGuiComboFlags_CustomPreview))
{
    // ...
    ImGui::EndCombo();
}
if (ImGui::BeginComboPreview())
{
    ImGui::ColorButton("##color", ImVec4(1.0f, 0.5f, 0.5f, 1.0f), ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoDragDrop, color_square_size);
    ImGui::TextUnformatted(items[item_current_idx]);
    ImGui::EndComboPreview();
}

image

The idea is that BeginComboPreview() will set cursor and clip rect, and EndComboPreview() restores things. Whereas most custom solutions above would leave the preview unclipped (so glitches would appear when resizing down). The clipping and drawcmd merging currently relies on ImGui:: side max cursor pos, but we will switch to use upcoming ImDrawCmd AABB when available.

If it is works well this may eventually be promoted to a public api (imgui.h)

ocornut avatar Jun 15 '21 13:06 ocornut

On full custom combo logic

With 060b6ee7d I did a small refactor of BeginCombo() to extract BeginComboPopup() out of it. The idea being that if you have to copy the content of BeginCombo() you are more likely to be able to reuse the BeginComboPopup() parts unchanged, and so your copy of BeginCombo() can be smaller (removed ~45 lines).

That said, I think it should now be possible to implement variety of filter idioms without copying/touching BeginCombo() (possibly by using SetItemAllowOverlap). I encourage you all to try and let me know.

ocornut avatar Jun 15 '21 13:06 ocornut

So, is it already possible to make a simple ComboFilter/Autocomplete widget using this additional functionality? Such useful widget, yet all provided "examples" in this thread and others are non-working/non-compiling.

the-goodies avatar Jun 16 '21 13:06 the-goodies

I don't know I haven't tried to make one. Perhaps something worth investigating for a demo.

ocornut avatar Jun 16 '21 13:06 ocornut

Cool. Will check this. By the way, I've spent a while to fix code from @kovewnikov Not sure if I did this best way, but works quite OK for me. Here's the code: imguiComboFilter.zip

ezgif com-video-to-gif-4

slajerek avatar Jun 16 '21 20:06 slajerek

I just stumbled upon how FlatUI handles the combo filtering, and its logic fits better with IMGUI mentality IMO.

I will try to put the filter inside the combo popup (and leave the header to behave exactly like current Combo). Will create a new snippet someday.

image

flat-ui demo: http://designmodo.github.io/Flat-UI/

ComboWithFilter

Source Code
namespace ImGui
{

    // https://github.com/forrestthewoods/lib_fts

    // Forward declarations for "private" implementation
    namespace fuzzy_internal {
        static bool fuzzy_match_recursive(const char* pattern, const char* str, int& outScore, const char* strBegin,
            uint8_t const* srcMatches, uint8_t* newMatches, int maxMatches, int nextMatch,
            int& recursionCount, int recursionLimit);
    }
    // Private implementation
    static bool fuzzy_internal::fuzzy_match_recursive(const char* pattern, const char* str, int& outScore,
        const char* strBegin, uint8_t const* srcMatches, uint8_t* matches, int maxMatches,
        int nextMatch, int& recursionCount, int recursionLimit)
    {
        // Count recursions
        ++recursionCount;
        if (recursionCount >= recursionLimit)
            return false;

        // Detect end of strings
        if (*pattern == '\0' || *str == '\0')
            return false;

        // Recursion params
        bool recursiveMatch = false;
        uint8_t bestRecursiveMatches[256];
        int bestRecursiveScore = 0;

        // Loop through pattern and str looking for a match
        bool first_match = true;
        while (*pattern != '\0' && *str != '\0') {

            // Found match
            if (tolower(*pattern) == tolower(*str)) {

                // Supplied matches buffer was too short
                if (nextMatch >= maxMatches)
                    return false;

                // "Copy-on-Write" srcMatches into matches
                if (first_match && srcMatches) {
                    memcpy(matches, srcMatches, nextMatch);
                    first_match = false;
                }

                // Recursive call that "skips" this match
                uint8_t recursiveMatches[256];
                int recursiveScore;
                if (fuzzy_match_recursive(pattern, str + 1, recursiveScore, strBegin, matches, recursiveMatches, sizeof(recursiveMatches), nextMatch, recursionCount, recursionLimit)) {

                    // Pick best recursive score
                    if (!recursiveMatch || recursiveScore > bestRecursiveScore) {
                        memcpy(bestRecursiveMatches, recursiveMatches, 256);
                        bestRecursiveScore = recursiveScore;
                    }
                    recursiveMatch = true;
                }

                // Advance
                matches[nextMatch++] = (uint8_t)(str - strBegin);
                ++pattern;
            }
            ++str;
        }

        // Determine if full pattern was matched
        bool matched = *pattern == '\0' ? true : false;

        // Calculate score
        if (matched) {
            const int sequential_bonus = 15;            // bonus for adjacent matches
            const int separator_bonus = 30;             // bonus if match occurs after a separator
            const int camel_bonus = 30;                 // bonus if match is uppercase and prev is lower
            const int first_letter_bonus = 15;          // bonus if the first letter is matched

            const int leading_letter_penalty = -5;      // penalty applied for every letter in str before the first match
            const int max_leading_letter_penalty = -15; // maximum penalty for leading letters
            const int unmatched_letter_penalty = -1;    // penalty for every letter that doesn't matter

            // Iterate str to end
            while (*str != '\0')
                ++str;

            // Initialize score
            outScore = 100;

            // Apply leading letter penalty
            int penalty = leading_letter_penalty * matches[0];
            if (penalty < max_leading_letter_penalty)
                penalty = max_leading_letter_penalty;
            outScore += penalty;

            // Apply unmatched penalty
            int unmatched = (int)(str - strBegin) - nextMatch;
            outScore += unmatched_letter_penalty * unmatched;

            // Apply ordering bonuses
            for (int i = 0; i < nextMatch; ++i) {
                uint8_t currIdx = matches[i];

                if (i > 0) {
                    uint8_t prevIdx = matches[i - 1];

                    // Sequential
                    if (currIdx == (prevIdx + 1))
                        outScore += sequential_bonus;
                }

                // Check for bonuses based on neighbor character value
                if (currIdx > 0) {
                    // Camel case
                    char neighbor = strBegin[currIdx - 1];
                    char curr = strBegin[currIdx];
                    if (::islower(neighbor) && ::isupper(curr))
                        outScore += camel_bonus;

                    // Separator
                    bool neighborSeparator = neighbor == '_' || neighbor == ' ';
                    if (neighborSeparator)
                        outScore += separator_bonus;
                }
                else {
                    // First letter
                    outScore += first_letter_bonus;
                }
            }
        }

        // Return best result
        if (recursiveMatch && (!matched || bestRecursiveScore > outScore)) {
            // Recursive score is better than "this"
            memcpy(matches, bestRecursiveMatches, maxMatches);
            outScore = bestRecursiveScore;
            return true;
        }
        else if (matched) {
            // "this" score is better than recursive
            return true;
        }
        else {
            // no match
            return false;
        }
    }



    static bool fuzzy_match(char const* pattern, char const* str, int& outScore, uint8_t* matches, int maxMatches) {
        int recursionCount = 0;
        int recursionLimit = 10;

        return fuzzy_internal::fuzzy_match_recursive(pattern, str, outScore, str, nullptr, matches, maxMatches, 0, recursionCount, recursionLimit);
    }

    // Public interface
    bool fuzzy_match_simple(char const* pattern, char const* str) {
        while (*pattern != '\0' && *str != '\0') {
            if (tolower(*pattern) == tolower(*str))
                ++pattern;
            ++str;
        }

        return *pattern == '\0' ? true : false;
    }

    bool fuzzy_match(char const* pattern, char const* str, int& outScore) {

        uint8_t matches[256];
        return fuzzy_match(pattern, str, outScore, matches, sizeof(matches));
    }

    static bool sortbysec_desc(const std::pair<int, int>& a, const std::pair<int, int>& b)
    {
        return (b.second < a.second);
    }

    bool ComboWithFilter(const char* label, int* current_item, const std::vector<std::string>& items)
    {
        ImGuiContext& g = *GImGui;

        ImGuiWindow* window = GetCurrentWindow();
        if (window->SkipItems)
            return false;

        const ImGuiStyle& style = g.Style;
        int items_count = items.size();

        // Call the getter to obtain the preview string which is a parameter to BeginCombo()
        const char* preview_value = NULL;
        if (*current_item >= 0 && *current_item < items_count)
            preview_value = items[*current_item].c_str();

        static char pattern_buffer[256] = { 0 };
        bool isNeedFilter = false;

        char comboButtonName[512] = { 0 };
        ImFormatString(comboButtonName, IM_ARRAYSIZE(comboButtonName), "%s##name_ComboWithFilter_button_%s", preview_value? preview_value:"", label);

        char name_popup[256 + 10];
        ImFormatString(name_popup, IM_ARRAYSIZE(name_popup), "##name_popup_%s", label);

        // Display items
        // FIXME-OPT: Use clipper (but we need to disable it on the appearing frame to make sure our call to SetItemDefaultFocus() is processed)
        bool value_changed = false;

        const float expected_w = CalcItemWidth();
        ImVec2 item_min = GetItemRectMin();
        bool isNewOpen = false;
        float sz = GetFrameHeight();
        ImVec2 size(sz, sz);
        ImVec2 CursorPos = window->DC.CursorPos;
        ImVec2 pos = CursorPos + ImVec2(expected_w-sz, 0);
        const ImRect bb(pos, pos + size);

        float ButtonTextAlignX = g.Style.ButtonTextAlign.x;
        g.Style.ButtonTextAlign.x = 0;
        if (ImGui::Button(comboButtonName, ImVec2(expected_w, 0)))
        {
            ImGui::OpenPopup(name_popup);
            isNewOpen = true;
        }
        g.Style.ButtonTextAlign.x = ButtonTextAlignX;
        bool hovered = IsItemHovered();
        bool active = IsItemActivated();
        bool pressed = IsItemClicked();

        // Render
        //const ImU32 bg_col = GetColorU32((active && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
        //RenderFrame(bb.Min, bb.Max, bg_col, true, g.Style.FrameRounding);
        const ImU32 text_col = GetColorU32(ImGuiCol_Text);
        RenderArrow(window->DrawList, bb.Min + ImVec2(ImMax(0.0f, (size.x - g.FontSize) * 0.5f), ImMax(0.0f, (size.y - g.FontSize) * 0.5f)), text_col, ImGuiDir_Down);

        if (isNewOpen)
        {
            memset(pattern_buffer, 0, IM_ARRAYSIZE(pattern_buffer));
        }
        ImVec2 item_max = GetItemRectMax();
        SetNextWindowPos({ CursorPos.x, item_max.y });
        ImGui::SetNextWindowSize({ ImGui::GetItemRectSize().x, 0 });
        if (ImGui::BeginPopup(name_popup))
        {
            ImGui::PushStyleColor(ImGuiCol_FrameBg, (ImVec4)ImColor(240, 240, 240, 255));
            ImGui::PushStyleColor(ImGuiCol_Text, (ImVec4)ImColor(0, 0, 0, 255));
            ImGui::PushItemWidth(-FLT_MIN);
            // Filter input
            if (isNewOpen)
                ImGui::SetKeyboardFocusHere();
            InputText("##ComboWithFilter_inputText", pattern_buffer, 256);

            // Search Icon, you can use it if you load IconsFontAwesome5 https://github.com/juliettef/IconFontCppHeaders
            //const ImVec2 label_size = CalcTextSize(ICON_FA_SEARCH, NULL, true);
            //const ImVec2 search_icon_pos(ImGui::GetItemRectMax().x - label_size.x - style.ItemInnerSpacing.x * 2, window->DC.CursorPos.y + style.FramePadding.y + g.FontSize * 0.1f);
            //RenderText(search_icon_pos, ICON_FA_SEARCH);

            ImGui::PopStyleColor(2);
            if (pattern_buffer[0] != '\0')
            {
                isNeedFilter = true;
            }

            std::vector<std::pair<int, int> > itemScoreVector;
            if (isNeedFilter)
            {
                for (int i = 0; i < items_count; i++)
                {
                    int score = 0;
                    bool matched = fuzzy_match(pattern_buffer, items[i].c_str(), score);
                    if (matched)
                        itemScoreVector.push_back(std::make_pair(i, score));
                }
                std::sort(itemScoreVector.begin(), itemScoreVector.end(), sortbysec_desc);
            }

            int show_count = isNeedFilter ? itemScoreVector.size() : items_count;
            if (ImGui::ListBoxHeader("##ComboWithFilter_itemList", show_count))
            {
                for (int i = 0; i < show_count; i++)
                {
                    int idx = isNeedFilter ? itemScoreVector[i].first : i;
                    PushID((void*)(intptr_t)idx);
                    const bool item_selected = (idx == *current_item);
                    const char* item_text = items[idx].c_str();
                    if (Selectable(item_text, item_selected))
                    {
                        value_changed = true;
                        *current_item = idx;
                        CloseCurrentPopup();
                    }
                    if (item_selected)
                        SetItemDefaultFocus();
                    PopID();
                }
                ImGui::ListBoxFooter();
            }
            ImGui::PopItemWidth();
            ImGui::EndPopup();
        }


        if (value_changed)
            MarkItemEdited(g.CurrentWindow->DC.LastItemId);

        return value_changed;
    }
}

InfResearch avatar Jul 25 '21 09:07 InfResearch

Edit: updated my version of ComboWithFilter for v1.92.0 WIP. Looks roughly the same as old gif below.

Added it to the imgui directx12 example as a demo in this branch.


Testing on imgui 1.78 WIP, I tried some of the above combo filters.

After fixing some deprecated functions, I still couldn't get r-lyeh's ComboFilter to accept any text (immediately loses focus). kovewnikov's and slajerek's versions work better, but not if there are multiple ComboFilters visible at once (then it won't accept more than one character). Possibly it's my fault since I used a static buffer and ComboFilterState.

ChenRenault's ComboWithFilter worked wonderfully. Putting the filter inside the combo makes the api simpler and I can have multiple visible at once.

I extended ComboWithFilter to add arrow navigation, Enter to confirm, and max_height_in_items. It uses BeginCombo to avoids drawing beyond the edge of the window and I fixed focus on open: https://gist.github.com/idbrii/5ddb2135ca122a0ec240ce046d9e6030

combo-filter

idbrii avatar Apr 01 '22 18:04 idbrii

Dear idbrii

hi I tried your source code on version 1.87. But it's not working properly. Can you test it on version 1.87? thank you.

kwonjinyoung avatar Apr 22 '22 08:04 kwonjinyoung

Hi @kwonjinyoung I left a comment on the gist because it did not work for me either in 1.87. I believe the changes that I've posted in the comment should be all that's necessary to get this to work in 1.87.

ambrosiogabe avatar May 05 '22 14:05 ambrosiogabe

@ocornut

On the topic of customizing the combo preview

I have pushed an experimental API (declared in imgui_internal.h)

If it is works well this may eventually be promoted to a public api (imgui.h)

Hey I cannot find this experimental branch, and I can't find this in master. Did this get removed?

SirDragonClaw avatar Jun 08 '22 08:06 SirDragonClaw

Its all still here in imgui_internal.h

ocornut avatar Jun 08 '22 08:06 ocornut

If somebody is interested I changed https://github.com/ocornut/imgui/issues/1658#issuecomment-862692546 into imgui_combo_autoselect.h and did a minor code cleanup, changed function signature as per old API Combo() with item_getter callback (in this way can be used in much more dynamic data context)

https://gist.github.com/ozlb/9cd35891aa4de3450e8e4c844837e7f9

const char* hints[] = {
				"AnimGraphNode_CopyBone",
				"ce skipaa",
				"ce skipscreen",
				"ce skipsplash",
				"ce skipsplashscreen",
				"client_unit.cpp",
				"letrograd",
				"level",
				"leveler",
				"MacroCallback.cpp",
				"Miskatonic university",
				"MockAI.h",
				"MockGameplayTasks.h",
				"MovieSceneColorTrack.cpp",
				"r.maxfps",
				"r.maxsteadyfps",
				"reboot",
				"rescale",
				"reset",
				"resource",
				"restart",
				"retrocomputer",
				"retrograd",
				"return",
				"slomo 10",
				"SVisualLoggerLogsList.h",
				"The Black Knight",
};
static int comboSelection = 0;
static char buf[128] = { 0x00 };
static char sel[128] = { 0x00 };
struct Funcs { static bool ItemGetter(void* data, int n, const char** out_str) { *out_str = ((const char**)data)[n]; return true; } };
if (ImGui::ComboAutoSelect("my combofilter", buf, IM_ARRAYSIZE(buf), &comboSelection, &Funcs::ItemGetter, hints, IM_ARRAYSIZE(hints), NULL)) {
	//...picking has occurred
	sprintf(sel, "%s", buf);
}
ImGui::Text("Selection: %s", sel);

ozlb avatar Aug 23 '22 21:08 ozlb