imgui
imgui copied to clipboard
Using BeginCombo() and avanced: custom preview, filtering
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

ImGuiComboFlags_NoPreview

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:

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");

Clicking the preview area will get you the normal combo popup, etc.
Hi,
Nice example! Is it possible to add an image before the label in the selection list, or in the selected choice?
Yes, they are regular popup so you can do anything within them.
I managed to display an image per item by calling a ImGui::Selectable("", is_selected);, followed by ImGui::SameLine(); and then showing image + text:

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.
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);

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.
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");

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.
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().
You are right of course =). GetTextLineHeight() works just fine here. Thanks!
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.

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.
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
I've updated the snippet above to a cleaner implementation. Also, here's latest gif anim (showcasing mouse & keyboardnav picking):

Thanks @r-lyeh for the code and gif, this is really nice!
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.

flat-ui demo: http://designmodo.github.io/Flat-UI/
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

missing a pic!
missing a pic!
Done
Using this thread and drawImage seems to work, thanks all!

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));
}
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)
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();
}

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)
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.
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.
I don't know I haven't tried to make one. Perhaps something worth investigating for a demo.
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

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.
flat-ui demo: http://designmodo.github.io/Flat-UI/

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;
}
}
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

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.
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.
@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?
Its all still here in imgui_internal.h
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);