Skip to content

Fix a bug in ImGuiTextFilter and extend it to be more customizable #2435

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 149 additions & 23 deletions imgui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2044,6 +2044,9 @@ void ImGuiStorage::SetAllInt(int v)

// Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]"
ImGuiTextFilter::ImGuiTextFilter(const char* default_filter)
: MatchMode(ImGuiTextFilterMode_Or)
, WordSplitter(',')
, MinWordSize(0)
{
if (default_filter)
{
Expand All @@ -2053,7 +2056,6 @@ ImGuiTextFilter::ImGuiTextFilter(const char* default_filter)
else
{
InputBuf[0] = 0;
CountGrep = 0;
}
}

Expand All @@ -2067,7 +2069,21 @@ bool ImGuiTextFilter::Draw(const char* label, float width)
return value_changed;
}

void ImGuiTextFilter::ImGuiTextRange::split(char separator, ImVector<ImGuiTextRange>* out) const
static void AddWordToTextRange(const char* begin, const char* end, ImVector<ImGuiTextFilter::ImGuiTextRange>* out, char minWordSize)
{
if (*begin == '-')
{
if ((end - begin) > minWordSize + 1)
out->push_front(ImGuiTextFilter::ImGuiTextRange(begin, end));
}
else
{
if ((end - begin) > minWordSize)
out->push_back(ImGuiTextFilter::ImGuiTextRange(begin, end));
}
}

void ImGuiTextFilter::ImGuiTextRange::split(char separator, ImVector<ImGuiTextRange>* out, char minWordSize) const
{
out->resize(0);
const char* wb = b;
Expand All @@ -2076,33 +2092,46 @@ void ImGuiTextFilter::ImGuiTextRange::split(char separator, ImVector<ImGuiTextRa
{
if (*we == separator)
{
out->push_back(ImGuiTextRange(wb, we));
AddWordToTextRange(wb, we, out, minWordSize);
wb = we + 1;
}
we++;
}
if (wb != we)
out->push_back(ImGuiTextRange(wb, we));

AddWordToTextRange(wb, we, out, minWordSize);
}

void ImGuiTextFilter::Build()
{
NegativeFilterCount = 0;
Filters.resize(0);
ImGuiTextRange input_range(InputBuf, InputBuf+strlen(InputBuf));
input_range.split(',', &Filters);
input_range.split(WordSplitter, &Filters, MinWordSize);

CountGrep = 0;
for (int i = 0; i != Filters.Size; i++)
{
ImGuiTextRange& f = Filters[i];
IM_ASSERT(!f.empty());

if (f.b[0] == '-')
{
IM_ASSERT(NegativeFilterCount == i);
NegativeFilterCount++;
f.b++;
}

while (f.b < f.e && ImCharIsBlankA(f.b[0]))
f.b++;
while (f.e > f.b && ImCharIsBlankA(f.e[-1]))
f.e--;

if (f.empty())
continue;
if (Filters[i].b[0] != '-')
CountGrep += 1;
{
Filters.erase(&f);
if (NegativeFilterCount == i)
NegativeFilterCount--;
i--;
}
}
}

Expand All @@ -2114,30 +2143,127 @@ bool ImGuiTextFilter::PassFilter(const char* text, const char* text_end) const
if (text == NULL)
text = "";

for (int i = 0; i != Filters.Size; i++)
int i = 0;
for (; i != NegativeFilterCount; i++)
{
const ImGuiTextRange& f = Filters[i];
if (f.empty())
continue;
if (f.b[0] == '-')

// Subtract
if (ImStristr(text, text_end, f.begin(), f.end()) != NULL)
return false;
}

// Implicit * grep
if (NegativeFilterCount == Filters.Size)
return true;

for (; i != Filters.Size; i++)
{
const ImGuiTextRange& f = Filters[i];

if (ImStristr(text, text_end, f.begin(), f.end()) != NULL)
{
// Subtract
if (ImStristr(text, text_end, f.b + 1, f.e) != NULL)
return false;
// in Or mode we stop testing after a single hit
if (MatchMode == ImGuiTextFilterMode_Or)
return true;
}
else
{
// Grep
if (ImStristr(text, text_end, f.b, f.e) != NULL)
return true;
// in And mode we stop testing after a single miss
if (MatchMode == ImGuiTextFilterMode_And)
return false;
}
}

return MatchMode == ImGuiTextFilterMode_And;
}

//-----------------------------------------------------------------------------
// [SECTION] ImGuiTextFilterMatch
//-----------------------------------------------------------------------------
ImGuiTextFilterMatch::ImGuiTextFilterMatch(const ImGuiTextFilter& filter)
: Filter(filter)
, MatchStates(0)
, MatchMask(0)
, MatchCount(filter.Filters.Size - filter.NegativeFilterCount)
, State(ImGuiTextFilterMatchResultNone)
{
MatchMask = (1 << MatchCount) - 1;
}

// duplicated so we don't add overhead to regular filters since it can add up quick when processing large amount of items
void ImGuiTextFilterMatch::PassFilter(const char* text, const char* text_end)
{
if (Filter.Filters.empty())
return;

// stop processing on any failure
if (State == ImGuiTextFilterMatchResultFail)
return;

// stop processing on any success with no negative filters
if (Filter.NegativeFilterCount == 0 && State == ImGuiTextFilterMatchResultPass)
return;

if (text == NULL)
text = "";

int i = 0;
for (; i != Filter.NegativeFilterCount; i++)
{
const ImGuiTextFilter::ImGuiTextRange& f = Filter.Filters[i];

// Subtract
if (ImStristr(text, text_end, f.begin(), f.end()) != NULL)
{
State = ImGuiTextFilterMatchResultFail;
return;
}
}

// Implicit * grep
if (CountGrep == 0)
return true;
if (Filter.NegativeFilterCount == Filter.Filters.Size)
{
State = ImGuiTextFilterMatchResultPass;
return;
}

return false;
// already passed all statements, no sense in continuing
if (State == ImGuiTextFilterMatchResultPass)
{
return;
}

for (; i != Filter.Filters.Size; i++)
{
const ImGuiTextFilter::ImGuiTextRange& f = Filter.Filters[i];

ImU64 test_bit = (1 << i) - Filter.NegativeFilterCount;
if ((MatchStates & test_bit) != 0)
{
continue;
}

if (ImStristr(text, text_end, f.begin(), f.end()) != NULL)
{
// in Or mode we stop testing after a single hit
if (Filter.MatchMode == ImGuiTextFilterMode_Or)
{
State = ImGuiTextFilterMatchResultPass;
return;
}
else
{
MatchStates |= test_bit;
}
}
}

// Check if all bits are set after each check
if (MatchStates == MatchMask)
{
State = ImGuiTextFilterMatchResultPass;
}
}

//-----------------------------------------------------------------------------
Expand Down
66 changes: 56 additions & 10 deletions imgui.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ struct ImGuiStorage; // Helper for key->value storage
struct ImGuiStyle; // Runtime data for styling/colors
struct ImGuiTextBuffer; // Helper to hold and append into a text buffer (~string builder)
struct ImGuiTextFilter; // Helper to parse and apply text filters (e.g. "aaaaa[,bbbbb][,ccccc]")
struct ImGuiTextFilterMatch; // Helper for ImGuiTextFilter to allow parsing of multiple string field with a single filter

// Enums/Flags (declared as int for compatibility with old C++, to allow using as flags and to not pollute the top of this file)
// - Tip: Use your programming IDE navigation facilities on the names in the _central column_ below to find the actual flags/enum lists!
Expand All @@ -149,6 +150,8 @@ typedef int ImGuiNavInput; // -> enum ImGuiNavInput_ // Enum: An
typedef int ImGuiMouseButton; // -> enum ImGuiMouseButton_ // Enum: A mouse button identifier (0=left, 1=right, 2=middle)
typedef int ImGuiMouseCursor; // -> enum ImGuiMouseCursor_ // Enum: A mouse cursor identifier
typedef int ImGuiStyleVar; // -> enum ImGuiStyleVar_ // Enum: A variable identifier for styling
typedef int ImGuiTextFilterMode; // -> enum ImGuiTextFilterMode_ // Enum: To control how text filter handles multiple words
typedef int ImGuiTextFilterMatchResult; // -> enum ImGuiTextFilterMatchResult_ // Enum: A match identifier to better control applying a text filter over multiple fields
typedef int ImDrawCornerFlags; // -> enum ImDrawCornerFlags_ // Flags: for ImDrawList::AddRect(), AddRectFilled() etc.
typedef int ImDrawListFlags; // -> enum ImDrawListFlags_ // Flags: for ImDrawList
typedef int ImFontAtlasFlags; // -> enum ImFontAtlasFlags_ // Flags: for ImFontAtlas
Expand Down Expand Up @@ -1678,6 +1681,12 @@ struct ImGuiOnceUponAFrame
operator bool() const { int current_frame = ImGui::GetFrameCount(); if (RefFrame == current_frame) return false; RefFrame = current_frame; return true; }
};

enum ImGuiTextFilterMode_
{
ImGuiTextFilterMode_Or, // A single word match will pass the filter
ImGuiTextFilterMode_And // All words must match to pass the filter
};

// Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]"
struct ImGuiTextFilter
{
Expand All @@ -1691,17 +1700,54 @@ struct ImGuiTextFilter
// [Internal]
struct ImGuiTextRange
{
const char* b;
const char* e;

ImGuiTextRange() { b = e = NULL; }
ImGuiTextRange(const char* _b, const char* _e) { b = _b; e = _e; }
bool empty() const { return b == e; }
IMGUI_API void split(char separator, ImVector<ImGuiTextRange>* out) const;
const char* b;
const char* e;

ImGuiTextRange() { b = e = NULL; }
ImGuiTextRange(const char* _b, const char* _e) { b = _b; e = _e; }
const char* begin() const { return b; }
const char* end () const { return e; }
bool empty() const { return b == e; }
IMGUI_API void split(char separator, ImVector<ImGuiTextRange>* out, char minWordSize = 0) const;
};
char InputBuf[256];
ImVector<ImGuiTextRange>Filters;
int CountGrep;
char InputBuf[256];
ImVector<ImGuiTextRange> Filters;
int NegativeFilterCount;

// [Configuration]
ImGuiTextFilterMode MatchMode; // if true then all words must match, otherwise any matching word will be a pass
char WordSplitter; // Character used to split user string, ',' by default
char MinWordSize; // Minimum number of characters before a word is used for matching, can help improve UX by avoiding mass matching against 1 or 2 characters
};

enum ImGuiTextFilterMatchResult_
{
ImGuiTextFilterMatchResultNone, // There have been no matches or failures
ImGuiTextFilterMatchResultFail, // The match explicitly failed because of a subtractive clause or because no positive matches passed
ImGuiTextFilterMatchResultPass, // The match explicitly passed because of positive match
};

// Helper: Extend a single ImGuiTextFilter to multiple fields. It tracks explicit pass/failure conditions and will
// stop processing text after an explicit failure.
// Usage:
// ImGuiTextFilterMatch match(filter);
// match.PassFilter(partial_text_to_match1);
// match.PassFilter(partial_text_to_match2);
// if (match) { ... }
struct ImGuiTextFilterMatch
{
ImGuiTextFilterMatch(const ImGuiTextFilter& filter);

IMGUI_API void PassFilter(const char* text, const char* text_end = NULL);

operator bool() const { return State == ImGuiTextFilterMatchResultPass; }

// [Internal]
const ImGuiTextFilter& Filter;
ImU64 MatchStates; // track which positive matches have been seen for And mode
ImU64 MatchMask;
int MatchCount;
ImGuiTextFilterMatchResult State;
};

// Helper: Growable text buffer for logging/accumulating text
Expand Down