-
-
Notifications
You must be signed in to change notification settings - Fork 10.4k
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
Using BeginCombo() and avanced: custom preview, filtering #1658
Comments
Hi, Nice example! |
Yes, they are regular popup so you can do anything within them. |
I managed to display an image per item by calling a 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 |
If you set the selectable label to "" make sure there is a
That's a trickier one unfortunately, thought you can draw inside the combo widget:
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 While I'm at it, what is the correct way to calculate the size of the button? I am using 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. |
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.
Here you probably want to just |
You are right of course =). |
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? 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 |
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! |
Done |
Using this thread and drawImage seems to work, thanks all!
|
Just letting you know. You're missing a ) at |
On the topic of customizing the combo previewI 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. If it is works well this may eventually be promoted to a public api (imgui.h) |
On full custom combo logicWith 060b6ee I did a small refactor of 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 |
Source Codenamespace 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.89 WIP. Looks the same as old gif below. 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 |
Dear idbrii hi |
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. |
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 #1658 (comment) 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); |
Hi @idbrii Thanks fo your code, howerver, it dose not support the CKJ characters, and the search icon is gone, can you fix? io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\msyh.ttc", 16.0f, NULL, io.Fonts->GetGlyphRangesChineseFull()); |
@idbrii And fixed an assertion crash, but the search icon is still not displayed. diff --git a/imgui/fts_fuzzy_match.h b/imgui/fts_fuzzy_match.h
index 523982e..2267dcb 100644
--- a/imgui/fts_fuzzy_match.h
+++ b/imgui/fts_fuzzy_match.h
@@ -184,7 +184,8 @@ namespace fts {
char neighbor = strBegin[currIdx - 1];
char curr = strBegin[currIdx];
- if (::islower(neighbor) && ::isupper(curr))
+ // fix for utf-8
+ if (neighbor > 0 && curr > 0 && ::islower(neighbor) && ::isupper(curr))
outScore += camel_bonus;
// Separator I also tried with @ozlb |
My attempt to imitate VSCode, source code by https://github.com/hnOsmium0001/imgui-command-palette Source Code source codeimgui_extentions.h #pragma once
#include <string>
#include <vector>
#include "imgui/imgui.h"
// #include "CCImguiMacros.h"
// namespace ccimgui {}
#ifdef __cplusplus
#define NS_CCIMGUI_BEGIN namespace ccimgui {
#define NS_CCIMGUI_END }
#define USING_NS_CCIMGUI using namespace ccimgui
#define NS_CCIMGUI ::ccimgui
#else
#define NS_CCIMGUI_BEGIN
#define NS_CCIMGUI_END
#define USING_NS_CCIMGUI
#define NS_CCIMGUI
#endif
NS_CCIMGUI_BEGIN
IMGUI_API bool FuzzySearch(char const* pattern, char const* src, int& outScore);
IMGUI_API bool FuzzySearch(char const* pattern, char const* src, int& outScore, uint8_t matches[], int maxMatches, int& outMatches);
IMGUI_API void InitExtentionsContext();
IMGUI_API bool Combo(const char* label, int* current_item, const std::vector<std::string>& items, int popup_max_height_in_items = -1);
IMGUI_API bool ComboWithFilter(const char* label, int* current_item, const std::vector<std::string>& items, int popup_max_height_in_items = -1);
IMGUI_API bool ListBox(const char* label, int* current_item, const std::vector<std::string>& items, int height_in_items = -1);
IMGUI_API bool ListBoxWithFilter(const char* label, int* current_item, const std::vector<std::string>& items, int height_in_items = -1);
NS_CCIMGUI_END
imgui_extentions.cpp #include <cctype>
#include <algorithm>
#include <cstring>
#include <limits>
#include <utility>
#include "imgui_extentions.h"
#include "imgui/imgui.h"
#ifndef IMGUI_DEFINE_MATH_OPERATORS
#define IMGUI_DEFINE_MATH_OPERATORS
#endif
#include "imgui/imgui_internal.h"
#ifdef IMGUI_ICONSFONT_ENABLED
// #include "IconsFontAwesome5.h"
#include "IconsMaterialDesign.h"
#endif
static size_t HashCString(const char* p)
{
size_t result = 0;
constexpr size_t kPrime = 31;
for (size_t i = 0; p[i] != '\0'; ++i) {
result = p[i] + (result * kPrime);
}
return result;
}
NS_CCIMGUI_BEGIN
namespace
{
bool FuzzySearchRecursive(const char* pattern, const char* src, int& outScore, const char* strBegin, const uint8_t srcMatches[], uint8_t newMatches[], int maxMatches, int& nextMatch, int& recursionCount, int recursionLimit);
} // namespace
bool FuzzySearch(char const* pattern, char const* haystack, int& outScore)
{
uint8_t matches[256];
int matchCount = 0;
return FuzzySearch(pattern, haystack, outScore, matches, sizeof(matches), matchCount);
}
bool FuzzySearch(char const* pattern, char const* haystack, int& outScore, uint8_t matches[], int maxMatches, int& outMatches)
{
int recursionCount = 0;
int recursionLimit = 10;
int newMatches = 0;
bool result = FuzzySearchRecursive(pattern, haystack, outScore, haystack, nullptr, matches, maxMatches, newMatches, recursionCount, recursionLimit);
outMatches = newMatches;
return result;
}
namespace
{
bool FuzzySearchRecursive(const char* pattern, const char* src, int& outScore, const char* strBegin, const uint8_t srcMatches[], uint8_t newMatches[], int maxMatches, int& nextMatch, int& recursionCount, int recursionLimit)
{
// Count recursions
++recursionCount;
if (recursionCount >= recursionLimit) {
return false;
}
// Detect end of strings
if (*pattern == '\0' || *src == '\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 firstMatch = true;
while (*pattern != '\0' && *src != '\0') {
// Found match
if (tolower(*pattern) == tolower(*src)) {
// Supplied matches buffer was too short
if (nextMatch >= maxMatches) {
return false;
}
// "Copy-on-Write" srcMatches into matches
if (firstMatch && srcMatches) {
memcpy(newMatches, srcMatches, nextMatch);
firstMatch = false;
}
// Recursive call that "skips" this match
uint8_t recursiveMatches[256];
int recursiveScore;
int recursiveNextMatch = nextMatch;
if (FuzzySearchRecursive(pattern, src + 1, recursiveScore, strBegin, newMatches, recursiveMatches, sizeof(recursiveMatches), recursiveNextMatch, recursionCount, recursionLimit)) {
// Pick the best recursive score
if (!recursiveMatch || recursiveScore > bestRecursiveScore) {
memcpy(bestRecursiveMatches, recursiveMatches, 256);
bestRecursiveScore = recursiveScore;
}
recursiveMatch = true;
}
// Advance
newMatches[nextMatch++] = (uint8_t)(src - strBegin);
++pattern;
}
++src;
}
// Determine if full pattern was matched
bool matched = *pattern == '\0';
// Calculate score
if (matched) {
const int sequentialBonus = 15; // bonus for adjacent matches
const int separatorBonus = 30; // bonus if match occurs after a separator
const int camelBonus = 30; // bonus if match is uppercase and prev is lower
const int firstLetterBonus = 15; // bonus if the first letter is matched
const int leadingLetterPenalty = -5; // penalty applied for every letter in str before the first match
const int maxLeadingLetterPenalty = -15; // maximum penalty for leading letters
const int unmatchedLetterPenalty = -1; // penalty for every letter that doesn't matter
// Iterate str to end
while (*src != '\0') {
++src;
}
// Initialize score
outScore = 100;
// Apply leading letter penalty
int penalty = leadingLetterPenalty * newMatches[0];
if (penalty < maxLeadingLetterPenalty) {
penalty = maxLeadingLetterPenalty;
}
outScore += penalty;
// Apply unmatched penalty
int unmatched = (int)(src - strBegin) - nextMatch;
outScore += unmatchedLetterPenalty * unmatched;
// Apply ordering bonuses
for (int i = 0; i < nextMatch; ++i) {
uint8_t currIdx = newMatches[i];
if (i > 0) {
uint8_t prevIdx = newMatches[i - 1];
// Sequential
if (currIdx == (prevIdx + 1))
outScore += sequentialBonus;
}
// 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 += camelBonus;
}
// Separator
bool neighborSeparator = neighbor == '_' || neighbor == ' ';
if (neighborSeparator) {
outScore += separatorBonus;
}
} else {
// First letter
outScore += firstLetterBonus;
}
}
}
// Return best result
if (recursiveMatch && (!matched || bestRecursiveScore > outScore)) {
// Recursive score is better than "this"
memcpy(newMatches, bestRecursiveMatches, maxMatches);
outScore = bestRecursiveScore;
return true;
} else if (matched) {
// "this" score is better than recursive
return true;
} else {
// no match
return false;
}
}
} // namespace
struct SearchResult
{
int ItemIndex;
int Score;
int MatchCount;
uint8_t Matches[32];
};
struct ItemExtraData
{
bool Hovered = false;
bool Held = false;
};
struct Instance
{
std::vector<ItemExtraData> ExtraData;
int CurrentSelectedItem = 0;
char pattern_buffer[256];
struct
{
bool RefreshSearch = false;
bool ClearSearch = false;
} PendingActions;
};
struct Context
{
ImGuiStorage Instances;
Instance* CurrentCommandPalette = nullptr;
};
static Context* gContext = nullptr;
// =================================================================
// API implementation
// =================================================================
Context* CreateExtentionsContext()
{
auto ctx = new Context();
if (!gContext) {
gContext = ctx;
}
return ctx;
}
void DestroyExtentionsContext(Context* context)
{
delete context;
}
void DestroyExtentionsContext()
{
DestroyExtentionsContext(gContext);
gContext = nullptr;
}
void SetCurrentExtentionsContext(Context* context)
{
gContext = context;
}
Context* GetCurrentExtentionsContext()
{
return gContext;
}
void InitExtentionsContext()
{
CreateExtentionsContext();
}
static bool sortbysec_desc(const std::pair<int, int>& a, const std::pair<int, int>& b)
{
return (b.second < a.second);
}
static float CalcMaxPopupHeightFromItemCount(int items_count)
{
ImGuiContext& g = *GImGui;
if (items_count <= 0)
return FLT_MAX;
return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2);
}
IMGUI_API bool ComboWithFilter(const char* label, int* current_item, const std::vector<std::string>& items, int popup_max_height_in_items)
{
ImGuiContext& g = *GImGui;
ImGuiWindow* window = ImGui::GetCurrentWindow();
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();
// The old Combo() API exposed "popup_max_height_in_items". The new more general BeginCombo() API doesn't have/need it, but we emulate it here.
if (popup_max_height_in_items != -1 && !(g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint))
ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
ImGuiComboFlags flags = ImGuiComboFlags_None;
ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.Flags;
g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
if (window->SkipItems)
return false;
const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label);
IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : ImGui::GetFrameHeight();
const ImVec2 label_size = ImGui::CalcTextSize(label, NULL, true);
const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ImGui::CalcItemWidth();
const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
ImGui::ItemSize(total_bb, style.FramePadding.y);
if (!ImGui::ItemAdd(total_bb, id, &bb))
return false;
// Open on click
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held);
const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, id);
bool popup_open = ImGui::IsPopupOpen(popup_id, ImGuiPopupFlags_None);
if ((pressed || g.NavActivateId == id) && !popup_open)
{
ImGui::OpenPopupEx(popup_id, ImGuiPopupFlags_None);
popup_open = true;
}
// Render shape
const ImU32 frame_col = ImGui::GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
const float value_x2 = ImMax(bb.Min.x, bb.Max.x - arrow_size);
ImGui::RenderNavHighlight(bb, id);
if (!(flags & ImGuiComboFlags_NoPreview))
window->DrawList->AddRectFilled(bb.Min, ImVec2(value_x2, bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersLeft);
if (!(flags & ImGuiComboFlags_NoArrowButton))
{
ImU32 bg_col = ImGui::GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
ImU32 text_col = ImGui::GetColorU32(ImGuiCol_Text);
window->DrawList->AddRectFilled(ImVec2(value_x2, bb.Min.y), bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersRight);
if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x)
ImGui::RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f);
}
ImGui::RenderFrameBorder(bb.Min, bb.Max, style.FrameRounding);
// Custom preview
if (flags & ImGuiComboFlags_CustomPreview)
{
g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y);
IM_ASSERT(preview_value == NULL || preview_value[0] == 0);
preview_value = NULL;
}
// Render preview and label
if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
{
if (g.LogEnabled)
ImGui::LogSetNextTextDecoration("{", "}");
ImGui::RenderTextClipped(bb.Min + style.FramePadding, ImVec2(value_x2, bb.Max.y), preview_value, NULL, NULL);
}
if (label_size.x > 0)
ImGui::RenderText(ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), label);
if (!popup_open)
return false;
g.NextWindowData.Flags = backup_next_window_data_flags;
if (!ImGui::IsPopupOpen(popup_id, ImGuiPopupFlags_None))
{
g.NextWindowData.ClearFlags();
return false;
}
// Set popup size
float bb_w = bb.GetWidth();
if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint)
{
g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, bb_w);
}
else
{
if ((flags & ImGuiComboFlags_HeightMask_) == 0)
flags |= ImGuiComboFlags_HeightRegular;
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one
int popup_max_height_in_items = -1;
if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8;
else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4;
else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20;
ImGui::SetNextWindowSizeConstraints(ImVec2(bb_w, 0.0f), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
}
// This is essentially a specialized version of BeginPopupEx()
char name[16];
ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.BeginPopupStack.Size); // Recycle windows based on depth
// Set position given a custom constraint (peak into expected window size so we can position it)
// FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function?
// FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()?
if (ImGuiWindow* popup_window = ImGui::FindWindowByName(name))
if (popup_window->WasActive)
{
// Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us.
ImVec2 size_expected = ImGui::CalcWindowNextAutoFitSize(popup_window);
popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)"
ImRect r_outer = ImGui::GetPopupAllowedExtentRect(popup_window);
ImVec2 pos = ImGui::FindBestWindowPosForPopupEx(bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, bb, ImGuiPopupPositionPolicy_ComboBox);
ImGui::SetNextWindowPos(pos);
}
// We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx()
ImGuiWindowFlags window_flags = ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(g.Style.FramePadding.x, g.Style.WindowPadding.y)); // Horizontally align ourselves with the framed text
bool ret = ImGui::Begin(name, NULL, window_flags);
ImGui::PopStyleVar();
if (!ret)
{
ImGui::EndPopup();
IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
return false;
}
bool value_changed = false;
auto& gg = *gContext;
auto& gi = *[&]() {
auto id = HashCString(label);
if (auto ptr = gg.Instances.GetVoidPtr(id)) {
return reinterpret_cast<Instance*>(ptr);
} else {
auto instance = new Instance();
gg.Instances.SetVoidPtr(id, instance);
return instance;
}
}();
static char pattern_buffer[256] = { 0 };
if (pressed)
memset(pattern_buffer, 0, IM_ARRAYSIZE(pattern_buffer));
bool isNeedFilter = false;
// Display items
{
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 (pressed)
ImGui::SetKeyboardFocusHere();
ImGui::InputText("##ComboWithFilter_inputText", pattern_buffer, 256);
#ifdef IMGUI_ICONSFONT_ENABLED
// Search Icon, you can use it if you load IconsFontAwesome5 https://github.com/juliettef/IconFontCppHeaders
const ImVec2 label_size = ImGui::CalcTextSize(ICON_MD_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);
ImGui::RenderText(search_icon_pos, ICON_MD_SEARCH);
#endif
ImGui::PopStyleColor(2);
if (pattern_buffer[0] != '\0')
isNeedFilter = true;
std::vector<SearchResult> SearchResults;
if (isNeedFilter)
{
for (int i = 0; i < items_count; i++)
{
SearchResult result;
if (FuzzySearch(pattern_buffer, items[i].c_str(), result.Score, result.Matches, IM_ARRAYSIZE(result.Matches), result.MatchCount)) {
result.ItemIndex = i;
SearchResults.push_back(result);
}
}
std::sort(SearchResults.begin(), SearchResults.end(), [](const SearchResult& a, const SearchResult& b) -> bool {
// We want the biggest element first
return a.Score > b.Score;
});
}
int show_count = isNeedFilter ? SearchResults.size() : items_count;
auto font_regular = ImGui::GetDrawListSharedData()->Font;
auto font_highlight = ImGui::GetDrawListSharedData()->Font;
ImU32 text_color_regular = ImGui::GetColorU32(ImGuiCol_Text);
ImU32 text_color_highlight = ImGui::ColorConvertFloat4ToU32(ImVec4(24.0f/255.0f, 163.0f/255.0f, 1.0f, 1.0f));
ImU32 item_hovered_color = ImGui::GetColorU32(ImGuiCol_HeaderHovered);
ImU32 item_active_color = ImGui::GetColorU32(ImGuiCol_HeaderActive);
ImU32 item_selected_color = ImGui::GetColorU32(ImGuiCol_Header);
if (gi.ExtraData.size() < show_count)
gi.ExtraData.resize(show_count);
bool select_focused_item = false;
// Calculate size from "height_in_items"
if (popup_max_height_in_items < 0)
popup_max_height_in_items = ImMin(show_count, 7);
float height_in_items_f = popup_max_height_in_items + 0.25f;
ImVec2 size(0.0f, ImFloor(ImGui::GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f));
if (ImGui::BeginListBox(label, size))
{
ImGuiWindow* window = ImGui::GetCurrentWindow();
ImDrawList* draw_list = window->DrawList;
const float font_size = window->CalcFontSize();
for (int i = 0; i < show_count; ++i) {
int idx = isNeedFilter ? SearchResults[i].ItemIndex : i;
const bool item_selected = (idx == *current_item);
const char* item_text = items[idx].c_str();
ImGuiID id = window->GetID(static_cast<int>(i));
ImGuiSelectableFlags flags = 0;
const ImVec2 size_arg(0.0f, 0.0f);
ImVec2 label_size = ImGui::CalcTextSize(item_text, NULL, true);
ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y);
ImVec2 pos = window->DC.CursorPos;
pos.y += window->DC.CurrLineTextBaseOffset;
// Fill horizontal space
// We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets.
const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0;
const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x;
const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x;
if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth))
size.x = ImMax(label_size.x, max_x - min_x);
// Text stays at the submission position, but bounding box may be extended on both sides
// const ImVec2 text_min = pos;
const ImVec2 text_max(min_x + size.x, pos.y + size.y);
// Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable.
ImRect rect(min_x, pos.y, text_max.x, text_max.y);
bool& hovered = gi.ExtraData[i].Hovered;
bool& held = gi.ExtraData[i].Held;
if (held && hovered) {
draw_list->AddRectFilled(rect.Min, rect.Max, item_active_color);
} else if (hovered) {
draw_list->AddRectFilled(rect.Min, rect.Max, item_hovered_color);
} else if (gi.CurrentSelectedItem == i) {
draw_list->AddRectFilled(rect.Min, rect.Max, item_selected_color);
}
if (isNeedFilter) {
// Iterating search results: draw text with highlights at matched chars
auto& search_result = SearchResults[i];
auto text_pos = window->DC.CursorPos;
int range_begin;
int range_end;
int last_range_end = 0;
auto DrawCurrentRange = [&]() {
if (range_begin != last_range_end) {
// Draw normal text between last highlighted range end and current highlighted range start
auto begin = item_text + last_range_end;
auto end = item_text + range_begin;
draw_list->AddText(text_pos, text_color_regular, begin, end);
auto segment_size = font_regular->CalcTextSizeA(font_size, std::numeric_limits<float>::max(), 0.0f, begin, end);
text_pos.x += segment_size.x;
}
auto begin = item_text + range_begin;
auto end = item_text + range_end;
draw_list->AddText(text_pos, text_color_highlight, begin, end);
auto segment_size = font_highlight->CalcTextSizeA(font_size, std::numeric_limits<float>::max(), 0.0f, begin, end);
text_pos.x += segment_size.x;
};
IM_ASSERT(search_result.MatchCount >= 1);
range_begin = search_result.Matches[0];
range_end = range_begin;
int last_char_idx = -1;
for (int j = 0; j < search_result.MatchCount; ++j) {
int char_idx = search_result.Matches[j];
if (char_idx == last_char_idx + 1) {
// These 2 indices are equal, extend our current range by 1
++range_end;
} else {
DrawCurrentRange();
last_range_end = range_end;
range_begin = char_idx;
range_end = char_idx + 1;
}
last_char_idx = char_idx;
}
// Draw the remaining range (if any)
if (range_begin != range_end) {
DrawCurrentRange();
}
// Draw the text after the last range (if any)
draw_list->AddText(text_pos, text_color_regular, item_text + range_end); // Draw until \0
} else {
// Iterating everything else: draw text as-is, there is no highlights
draw_list->AddText(pos, text_color_regular, item_text);
}
ImGui::ItemSize(rect);
if (ImGui::ItemAdd(rect, id)) {
if (ImGui::ButtonBehavior(rect, id, &hovered, &held)) {
gi.CurrentSelectedItem = i;
select_focused_item = true;
value_changed = true;
*current_item = idx;
ImGui::CloseCurrentPopup();
}
}
if (item_selected)
{
ImGui::SetItemDefaultFocus();
if (g.CurrentWindow->Appearing)
{
ImGui::SetScrollHereY(0.5f);
}
}
}
ImGui::EndListBox();
}
ImGui::PopItemWidth();
}
ImGui::EndPopup();
if (value_changed)
ImGui::MarkItemEdited(g.LastItemData.ID);
return value_changed;
}
IMGUI_API bool Combo(const char* label, int* current_item, const std::vector<std::string>& items, int popup_max_height_in_items)
{
int items_count = items.size();
ImGuiContext& g = *GImGui;
// 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();
// The old Combo() API exposed "popup_max_height_in_items". The new more general BeginCombo() API doesn't have/need it, but we emulate it here.
if (popup_max_height_in_items != -1 && !(g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint))
ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items)));
if (!ImGui::BeginCombo(label, preview_value, ImGuiComboFlags_None))
return false;
// 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;
for (int i = 0; i < items_count; i++)
{
ImGui::PushID((void*)(intptr_t)i);
const bool item_selected = (i == *current_item);
const char* item_text = items[i].c_str();
if (ImGui::Selectable(item_text, item_selected))
{
value_changed = true;
*current_item = i;
}
if (item_selected)
ImGui::SetItemDefaultFocus();
ImGui::PopID();
}
ImGui::EndCombo();
if (value_changed)
ImGui::MarkItemEdited(g.LastItemData.ID);
return value_changed;
}
IMGUI_API bool ListBox(const char* label, int* current_item, const std::vector<std::string>& items, int height_in_items)
{
int items_count = items.size();
ImGuiContext& g = *GImGui;
// Calculate size from "height_in_items"
if (height_in_items < 0)
height_in_items = ImMin(items_count, 7);
float height_in_items_f = height_in_items + 0.25f;
ImVec2 size(0.0f, ImFloor(ImGui::GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f));
if (!ImGui::BeginListBox(label, size))
return false;
// Assume all items have even height (= 1 line of text). If you need items of different height,
// you can create a custom version of ListBox() in your code without using the clipper.
bool value_changed = false;
ImGuiListClipper clipper;
clipper.Begin(items_count, ImGui::GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to.
while (clipper.Step())
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
{
const char* item_text = items[i].c_str();
ImGui::PushID(i);
const bool item_selected = (i == *current_item);
if (ImGui::Selectable(item_text, item_selected))
{
*current_item = i;
value_changed = true;
}
if (item_selected)
ImGui::SetItemDefaultFocus();
ImGui::PopID();
}
ImGui::EndListBox();
if (value_changed)
ImGui::MarkItemEdited(g.LastItemData.ID);
return value_changed;
}
IMGUI_API bool ListBoxWithFilter(const char* label, int* current_item, const std::vector<std::string>& items, int height_in_items)
{
int items_count = items.size();
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
// Calculate size from "height_in_items"
if (height_in_items < 0)
height_in_items = ImMin(items_count, 7);
float height_in_items_f = height_in_items + 0.25f;
ImVec2 size(0.0f, ImFloor(ImGui::GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f));
bool value_changed = false;
auto& gg = *gContext;
auto& gi = *[&]() {
auto id = HashCString(label);
if (auto ptr = gg.Instances.GetVoidPtr(id)) {
return reinterpret_cast<Instance*>(ptr);
} else {
auto instance = new Instance();
gg.Instances.SetVoidPtr(id, instance);
return instance;
}
}();
ImGuiWindow* window = ImGui::GetCurrentWindow();
bool isNeedFilter = false;
// Display items
{
ImGui::PushStyleColor(ImGuiCol_FrameBg, (ImVec4)ImColor(240, 240, 240, 255));
ImGui::PushStyleColor(ImGuiCol_Text, (ImVec4)ImColor(0, 0, 0, 255));
ImGui::PushItemWidth(-FLT_MIN);
ImGui::InputText("##ComboWithFilter_inputText", gi.pattern_buffer, 256);
#ifdef IMGUI_ICONSFONT_ENABLED
// Search Icon, you can use it if you load IconsFontAwesome5 https://github.com/juliettef/IconFontCppHeaders
const ImVec2 label_size = ImGui::CalcTextSize(ICON_MD_SEARCH, NULL, true);
const ImVec2 search_icon_pos(ImGui::GetItemRectMax().x - label_size.x - style.ItemInnerSpacing.x * 2, window->DC.CursorPos.y - ImGui::GetTextLineHeightWithSpacing());
ImGui::RenderText(search_icon_pos, ICON_MD_SEARCH);
#endif
ImGui::PopStyleColor(2);
if (gi.pattern_buffer[0] != '\0')
isNeedFilter = true;
std::vector<SearchResult> SearchResults;
if (isNeedFilter)
{
for (int i = 0; i < items_count; i++)
{
SearchResult result;
if (FuzzySearch(gi.pattern_buffer, items[i].c_str(), result.Score, result.Matches, IM_ARRAYSIZE(result.Matches), result.MatchCount)) {
result.ItemIndex = i;
SearchResults.push_back(result);
}
}
std::sort(SearchResults.begin(), SearchResults.end(), [](const SearchResult& a, const SearchResult& b) -> bool {
// We want the biggest element first
return a.Score > b.Score;
});
}
int show_count = isNeedFilter ? SearchResults.size() : items_count;
auto font_regular = ImGui::GetDrawListSharedData()->Font;
auto font_highlight = ImGui::GetDrawListSharedData()->Font;
ImU32 text_color_regular = ImGui::GetColorU32(ImGuiCol_Text);
ImU32 text_color_highlight = ImGui::ColorConvertFloat4ToU32(ImVec4(24.0f/255.0f, 163.0f/255.0f, 1.0f, 1.0f));
ImU32 item_hovered_color = ImGui::GetColorU32(ImGuiCol_HeaderHovered);
ImU32 item_active_color = ImGui::GetColorU32(ImGuiCol_HeaderActive);
ImU32 item_selected_color = ImGui::GetColorU32(ImGuiCol_Header);
if (gi.ExtraData.size() < show_count)
gi.ExtraData.resize(show_count);
bool select_focused_item = false;
if (ImGui::BeginListBox(label, size))
{
ImGuiWindow* window = ImGui::GetCurrentWindow();
ImDrawList* draw_list = window->DrawList;
const float font_size = window->CalcFontSize();
for (int i = 0; i < show_count; ++i) {
int idx = isNeedFilter ? SearchResults[i].ItemIndex : i;
const bool item_selected = (idx == *current_item);
const char* item_text = items[idx].c_str();
ImGuiID id = window->GetID(static_cast<int>(i));
ImGuiSelectableFlags flags = 0;
const ImVec2 size_arg(0.0f, 0.0f);
ImVec2 label_size = ImGui::CalcTextSize(item_text, NULL, true);
ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y);
ImVec2 pos = window->DC.CursorPos;
pos.y += window->DC.CurrLineTextBaseOffset;
// Fill horizontal space
// We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets.
const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0;
const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x;
const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x;
if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth))
size.x = ImMax(label_size.x, max_x - min_x);
// Text stays at the submission position, but bounding box may be extended on both sides
// const ImVec2 text_min = pos;
const ImVec2 text_max(min_x + size.x, pos.y + size.y);
// Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable.
ImRect rect(min_x, pos.y, text_max.x, text_max.y);
bool& hovered = gi.ExtraData[i].Hovered;
bool& held = gi.ExtraData[i].Held;
if (held && hovered) {
draw_list->AddRectFilled(rect.Min, rect.Max, item_active_color);
} else if (hovered) {
draw_list->AddRectFilled(rect.Min, rect.Max, item_hovered_color);
} else if (gi.CurrentSelectedItem == i) {
draw_list->AddRectFilled(rect.Min, rect.Max, item_selected_color);
}
if (isNeedFilter) {
// Iterating search results: draw text with highlights at matched chars
auto& search_result = SearchResults[i];
auto text_pos = window->DC.CursorPos;
int range_begin;
int range_end;
int last_range_end = 0;
auto DrawCurrentRange = [&]() {
if (range_begin != last_range_end) {
// Draw normal text between last highlighted range end and current highlighted range start
auto begin = item_text + last_range_end;
auto end = item_text + range_begin;
draw_list->AddText(text_pos, text_color_regular, begin, end);
auto segment_size = font_regular->CalcTextSizeA(font_size, std::numeric_limits<float>::max(), 0.0f, begin, end);
text_pos.x += segment_size.x;
}
auto begin = item_text + range_begin;
auto end = item_text + range_end;
draw_list->AddText(text_pos, text_color_highlight, begin, end);
auto segment_size = font_highlight->CalcTextSizeA(font_size, std::numeric_limits<float>::max(), 0.0f, begin, end);
text_pos.x += segment_size.x;
};
IM_ASSERT(search_result.MatchCount >= 1);
range_begin = search_result.Matches[0];
range_end = range_begin;
int last_char_idx = -1;
for (int j = 0; j < search_result.MatchCount; ++j) {
int char_idx = search_result.Matches[j];
if (char_idx == last_char_idx + 1) {
// These 2 indices are equal, extend our current range by 1
++range_end;
} else {
DrawCurrentRange();
last_range_end = range_end;
range_begin = char_idx;
range_end = char_idx + 1;
}
last_char_idx = char_idx;
}
// Draw the remaining range (if any)
if (range_begin != range_end) {
DrawCurrentRange();
}
// Draw the text after the last range (if any)
draw_list->AddText(text_pos, text_color_regular, item_text + range_end); // Draw until \0
} else {
// Iterating everything else: draw text as-is, there is no highlights
draw_list->AddText(pos, text_color_regular, item_text);
}
ImGui::ItemSize(rect);
if (ImGui::ItemAdd(rect, id)) {
if (ImGui::ButtonBehavior(rect, id, &hovered, &held)) {
gi.CurrentSelectedItem = i;
select_focused_item = true;
value_changed = true;
*current_item = idx;
ImGui::CloseCurrentPopup();
}
}
if (item_selected)
{
ImGui::SetItemDefaultFocus();
if (g.CurrentWindow->Appearing)
{
ImGui::SetScrollHereY(0.5f);
}
}
}
ImGui::EndListBox();
}
ImGui::PopItemWidth();
}
if (value_changed)
ImGui::MarkItemEdited(g.LastItemData.ID);
return value_changed;
}
NS_CCIMGUI_END |
I have improved @ozlb version of ComboAutoSelect as follow
Demo static ImGui::ComboAutoSelectData data = {{
"",
"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",
}};
if (ImGui::ComboAutoSelect("my combofilter", data)) {
// selection occurred
}
ImGui::Text("Selection: %s, index = %d", data.input, data.index); Source files |
@sweihub The magnifying glass isn't part of imgui or standard fonts. I use imgui from within bgfx and they setup several icon fonts as part of their build:
These fonts use the unicode private use area, so they shouldn't conflict with your CJK fonts. You can see how bgfx do it by following their use of s_iconsKenneyTtf. It's packed into a header (I assume to avoid including assets with their lib). If you're already familiar with setting up imgui fonts, then it might be easier to add the font yourself. Here's the font in the bgfx repo. See more information about the font in nicodinh/kenney-icon-font. fts_fuzzy_match is maintained here could you report the issue and the fix there? I don't quite understand what went wrong for you (maybe two issues: not displaying CJK and crashing when filtering for CJK characters?). |
Thanks, this is a great topic, I'll definitely try to integrate these with my own code, you guys rock. Last time when I integrated this that simple UX case did not work: press TAB, press TAB, press Enter and select Dropdown, Enter text, press Enter. |
I tried to improved @sweihub's ComboAutoSelect by:
Of course, there are some cons to this:
Example: struct Pair
{
std::string Name;
int IDNumber;
};
class ComboFilterPair : public ComboFilter<Pair, ComboFilterPair>
{
public:
ComboFilterPair(std::string_view combo_name, std::span<Pair> combo_items) :
ComboFilter(combo_name, combo_items)
{}
// This is the getter function for the span of objects you need to represent as string
// You can also make ComboFilter a friend of the inheriting class if you need GetItemString function to be private
const char* GetItemString(int index) const
{
return Items[index].Name.c_str();
}
};
static std::vector<Pair> vec_pair = { {"Pair1", 1}, {"PairTwo", 2}, {"PairNumber3", 3}, {"FourthPair", 4}, {"5thPair", 5} };
static ComboFilterPair combo_filter("Pair Combo Box", vec_pair);
if (combo_filter.Render()) {
} https://gist.github.com/khlorz/5fbc89fa0c179341c6465fbca1425510 |
Fixing my previous bad attempt at improving ComboAutoSelect #1658 (comment):
#include <vector>
static const char* item_getter(const std::vector<std::string>& items, int index) {
if (index >= 0 && index < (int)items.size()) {
return items[index].c_str();
}
return "N/A";
}
static std::vector<std::string> items{ "instruction", "Chemistry", "Beating Around the Bush", "Instantaneous Combustion", "Level 999999", "nasal problems", "On cloud nine", "break the iceberg", "lacircificane" };
static char inputbuf[128];
static int selected_item = -1;
if (ImGui::ComboAutoSelect("std::vector combo", inputbuf, 128, selected_item, items, item_getter, ImGuiComboFlags_HeightSmall)) {
/* Selection made */
}
|
As of today, I would say that none of the examples above work out of the box with the current version of ImGui. In some cases you may be able to get things to work but only after replacing deprecated functions. PS. I didn't try the last implementation posted here because it uses C++ templates. That one might work for you. |
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.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.
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:
Clicking the preview area will get you the normal combo popup, etc.
The text was updated successfully, but these errors were encountered: