Skip to content

Commit ed66ca5

Browse files
committed
Extend ImGuiTextFilter to be customizable and usable for multiple fields. Preserved current behavior and performance.
* Fixed negative matches not working when located after a positive matches. Optimized by keeping all negative matches at the front of the filter list. * Add the following customization options: ** Custom split character, default is still ',' ** Minimum word match length can be used to keep the search more stable for the first couple characters of input ** Match mode can be switch from Or (any pass causes a success) to And (all positive filters must match). * Add a utility to allow applying a single filter to multiple fields.
1 parent 221bf93 commit ed66ca5

File tree

2 files changed

+195
-26
lines changed

2 files changed

+195
-26
lines changed

imgui.cpp

Lines changed: 149 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,7 +1333,7 @@ const char* ImStrchrRange(const char* str, const char* str_end, char c)
13331333

13341334
int ImStrlenW(const ImWchar* str)
13351335
{
1336-
//return (int)wcslen((const wchar_t*)str); // FIXME-OPT: Could use this when wchar_t are 16-bits
1336+
//return (int)wcslen((const wchar_t*)str); // FIXME-OPT: Could use this when wchar_t are 16-bits
13371337
int n = 0;
13381338
while (*str++) n++;
13391339
return n;
@@ -2014,6 +2014,9 @@ void ImGuiStorage::SetAllInt(int v)
20142014

20152015
// Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]"
20162016
ImGuiTextFilter::ImGuiTextFilter(const char* default_filter)
2017+
: WordSplitter(',')
2018+
, MatchMode(ImGuiTextFilterMode_Or)
2019+
, MinWordSize(0)
20172020
{
20182021
if (default_filter)
20192022
{
@@ -2023,7 +2026,6 @@ ImGuiTextFilter::ImGuiTextFilter(const char* default_filter)
20232026
else
20242027
{
20252028
InputBuf[0] = 0;
2026-
CountGrep = 0;
20272029
}
20282030
}
20292031

@@ -2039,7 +2041,21 @@ bool ImGuiTextFilter::Draw(const char* label, float width)
20392041
return value_changed;
20402042
}
20412043

2042-
void ImGuiTextFilter::TextRange::split(char separator, ImVector<TextRange>* out) const
2044+
static void AddWordToTextRange(const char* begin, const char* end, ImVector<ImGuiTextFilter::TextRange>* out, char minWordSize)
2045+
{
2046+
if (*begin == '-')
2047+
{
2048+
if ((end - begin) > minWordSize + 1)
2049+
out->push_front(ImGuiTextFilter::TextRange(begin, end));
2050+
}
2051+
else
2052+
{
2053+
if ((end - begin) > minWordSize)
2054+
out->push_back(ImGuiTextFilter::TextRange(begin, end));
2055+
}
2056+
}
2057+
2058+
void ImGuiTextFilter::TextRange::split(char separator, ImVector<TextRange>* out, char minWordSize) const
20432059
{
20442060
out->resize(0);
20452061
const char* wb = b;
@@ -2048,33 +2064,46 @@ void ImGuiTextFilter::TextRange::split(char separator, ImVector<TextRange>* out)
20482064
{
20492065
if (*we == separator)
20502066
{
2051-
out->push_back(TextRange(wb, we));
2067+
AddWordToTextRange(wb, we, out, minWordSize);
20522068
wb = we + 1;
20532069
}
20542070
we++;
20552071
}
2056-
if (wb != we)
2057-
out->push_back(TextRange(wb, we));
2072+
2073+
AddWordToTextRange(wb, we, out, minWordSize);
20582074
}
20592075

20602076
void ImGuiTextFilter::Build()
20612077
{
2078+
NegativeFilterCount = 0;
20622079
Filters.resize(0);
20632080
TextRange input_range(InputBuf, InputBuf+strlen(InputBuf));
2064-
input_range.split(',', &Filters);
2081+
input_range.split(WordSplitter, &Filters, MinWordSize);
20652082

2066-
CountGrep = 0;
20672083
for (int i = 0; i != Filters.Size; i++)
20682084
{
20692085
TextRange& f = Filters[i];
2086+
IM_ASSERT(!f.empty());
2087+
2088+
if (f.b[0] == '-')
2089+
{
2090+
IM_ASSERT(NegativeFilterCount == i);
2091+
NegativeFilterCount++;
2092+
f.b++;
2093+
}
2094+
20702095
while (f.b < f.e && ImCharIsBlankA(f.b[0]))
20712096
f.b++;
20722097
while (f.e > f.b && ImCharIsBlankA(f.e[-1]))
20732098
f.e--;
2099+
20742100
if (f.empty())
2075-
continue;
2076-
if (Filters[i].b[0] != '-')
2077-
CountGrep += 1;
2101+
{
2102+
Filters.erase(&f);
2103+
if (NegativeFilterCount == i)
2104+
NegativeFilterCount--;
2105+
i--;
2106+
}
20782107
}
20792108
}
20802109

@@ -2086,30 +2115,126 @@ bool ImGuiTextFilter::PassFilter(const char* text, const char* text_end) const
20862115
if (text == NULL)
20872116
text = "";
20882117

2089-
for (int i = 0; i != Filters.Size; i++)
2118+
int i = 0;
2119+
for (; i != NegativeFilterCount; i++)
20902120
{
20912121
const TextRange& f = Filters[i];
2092-
if (f.empty())
2093-
continue;
2094-
if (f.b[0] == '-')
2122+
2123+
// Subtract
2124+
if (ImStristr(text, text_end, f.begin(), f.end()) != NULL)
2125+
return false;
2126+
}
2127+
2128+
// Implicit * grep
2129+
if (NegativeFilterCount == Filters.Size)
2130+
return true;
2131+
2132+
for (; i != Filters.Size; i++)
2133+
{
2134+
const TextRange& f = Filters[i];
2135+
2136+
if (ImStristr(text, text_end, f.begin(), f.end()) != NULL)
20952137
{
2096-
// Subtract
2097-
if (ImStristr(text, text_end, f.begin()+1, f.end()) != NULL)
2098-
return false;
2138+
// in Or mode we stop testing after a single hit
2139+
if (MatchMode == ImGuiTextFilterMode_Or)
2140+
return true;
20992141
}
21002142
else
21012143
{
2102-
// Grep
2103-
if (ImStristr(text, text_end, f.begin(), f.end()) != NULL)
2104-
return true;
2144+
// in And mode we stop testing after a single miss
2145+
if (MatchMode == ImGuiTextFilterMode_And)
2146+
return false;
2147+
}
2148+
}
2149+
2150+
return true;
2151+
}
2152+
2153+
//-----------------------------------------------------------------------------
2154+
// [SECTION] ImGuiTextFilterMatch
2155+
//-----------------------------------------------------------------------------
2156+
ImGuiTextFilterMatch::ImGuiTextFilterMatch(const ImGuiTextFilter& filter)
2157+
: Filter(filter)
2158+
, State(ImGuiTextFilterMatchResultNone)
2159+
, MatchStates(0)
2160+
, MatchCount(filter.Filters.Size - filter.NegativeFilterCount)
2161+
, MatchMask((1 << MatchCount) - 1)
2162+
{
2163+
}
2164+
2165+
// duplicated so we don't add overhead to regular filters since it can add up quick when processing large amount of items
2166+
void ImGuiTextFilterMatch::PassFilter(const char* text, const char* text_end)
2167+
{
2168+
if (Filter.Filters.empty())
2169+
return;
2170+
2171+
// stop processing on any failure
2172+
if (State == ImGuiTextFilterMatchResultFail)
2173+
return;
2174+
2175+
// stop processing on any success with no negative filters
2176+
if (Filter.NegativeFilterCount == 0 && State == ImGuiTextFilterMatchResultPass)
2177+
return;
2178+
2179+
if (text == NULL)
2180+
text = "";
2181+
2182+
int i = 0;
2183+
for (; i != Filter.NegativeFilterCount; i++)
2184+
{
2185+
const ImGuiTextFilter::TextRange& f = Filter.Filters[i];
2186+
2187+
// Subtract
2188+
if (ImStristr(text, text_end, f.begin(), f.end()) != NULL)
2189+
{
2190+
State = ImGuiTextFilterMatchResultFail;
2191+
return;
21052192
}
21062193
}
21072194

21082195
// Implicit * grep
2109-
if (CountGrep == 0)
2110-
return true;
2196+
if (Filter.NegativeFilterCount == Filter.Filters.Size)
2197+
{
2198+
State = ImGuiTextFilterMatchResultPass;
2199+
return;
2200+
}
21112201

2112-
return false;
2202+
// already passed all statements, no sense in continuing
2203+
if (State == ImGuiTextFilterMatchResultPass)
2204+
{
2205+
return;
2206+
}
2207+
2208+
for (; i != Filter.Filters.Size; i++)
2209+
{
2210+
const ImGuiTextFilter::TextRange& f = Filter.Filters[i];
2211+
2212+
ImU64 test_bit = (1 << i) - Filter.NegativeFilterCount;
2213+
if ((MatchStates & test_bit) != 0)
2214+
{
2215+
continue;
2216+
}
2217+
2218+
if (ImStristr(text, text_end, f.begin(), f.end()) != NULL)
2219+
{
2220+
// in Or mode we stop testing after a single hit
2221+
if (Filter.MatchMode == ImGuiTextFilterMode_Or)
2222+
{
2223+
State = ImGuiTextFilterMatchResultPass;
2224+
return;
2225+
}
2226+
else
2227+
{
2228+
MatchStates |= test_bit;
2229+
}
2230+
}
2231+
}
2232+
2233+
// Check if all bits are set after each check
2234+
if (MatchStates == MatchMask)
2235+
{
2236+
State = ImGuiTextFilterMatchResultPass;
2237+
}
21132238
}
21142239

21152240
//-----------------------------------------------------------------------------

imgui.h

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ struct ImGuiStorage; // Helper for key->value storage
113113
struct ImGuiStyle; // Runtime data for styling/colors
114114
struct ImGuiTextBuffer; // Helper to hold and append into a text buffer (~string builder)
115115
struct ImGuiTextFilter; // Helper to parse and apply text filters (e.g. "aaaaa[,bbbb][,ccccc]")
116+
struct ImGuiTextFilterMatch; // Helper for ImGuiTextFilter to allow parsing of multiple string field with a single filter
116117

117118
// Typedefs and Enums/Flags (declared as int for compatibility with old C++, to allow using as flags and to not pollute the top of this file)
118119
// Use your programming IDE "Go to definition" facility on the names of the center columns to find the actual flags/enum lists.
@@ -129,6 +130,8 @@ typedef int ImGuiKey; // -> enum ImGuiKey_ // Enum: A
129130
typedef int ImGuiNavInput; // -> enum ImGuiNavInput_ // Enum: An input identifier for navigation
130131
typedef int ImGuiMouseCursor; // -> enum ImGuiMouseCursor_ // Enum: A mouse cursor identifier
131132
typedef int ImGuiStyleVar; // -> enum ImGuiStyleVar_ // Enum: A variable identifier for styling
133+
typedef int ImGuiTextFilterMode; // -> enum ImGuiTextFilterMode_ // Enum: To control how text filter handles multiple words
134+
typedef int ImGuiTextFilterMatchResult; // -> enum ImGuiTextFilterMatchResult_ // Enum: A match identifier to better control applying a text filter over multiple fields
132135
typedef int ImDrawCornerFlags; // -> enum ImDrawCornerFlags_ // Flags: for ImDrawList::AddRect*() etc.
133136
typedef int ImDrawListFlags; // -> enum ImDrawListFlags_ // Flags: for ImDrawList
134137
typedef int ImFontAtlasFlags; // -> enum ImFontAtlasFlags_ // Flags: for ImFontAtlas
@@ -1572,6 +1575,12 @@ struct ImGuiOnceUponAFrame
15721575
#define IMGUI_ONCE_UPON_A_FRAME static ImGuiOnceUponAFrame imgui_oaf; if (imgui_oaf) // OBSOLETED in 1.51, will remove!
15731576
#endif
15741577

1578+
enum ImGuiTextFilterMode_
1579+
{
1580+
ImGuiTextFilterMode_Or, // A single word match will pass the filter
1581+
ImGuiTextFilterMode_And // All words must match to pass the filter
1582+
};
1583+
15751584
// Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]"
15761585
struct ImGuiTextFilter
15771586
{
@@ -1593,11 +1602,46 @@ struct ImGuiTextFilter
15931602
const char* begin() const { return b; }
15941603
const char* end () const { return e; }
15951604
bool empty() const { return b == e; }
1596-
IMGUI_API void split(char separator, ImVector<TextRange>* out) const;
1605+
IMGUI_API void split(char separator, ImVector<TextRange>* out, char minWordSize = 0) const;
15971606
};
15981607
char InputBuf[256];
15991608
ImVector<TextRange> Filters;
1600-
int CountGrep;
1609+
int NegativeFilterCount;
1610+
1611+
// [Configuration]
1612+
ImGuiTextFilterMode MatchMode; // if true then all words must match, otherwise any matching word will be a pass
1613+
char WordSplitter; // Character used to split user string, ',' by default
1614+
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
1615+
};
1616+
1617+
enum ImGuiTextFilterMatchResult_
1618+
{
1619+
ImGuiTextFilterMatchResultNone, // There have been no matches or failures
1620+
ImGuiTextFilterMatchResultFail, // The match explicitly failed because of a subtractive clause or because no positive matches passed
1621+
ImGuiTextFilterMatchResultPass, // The match explicitly passed because of positive match
1622+
};
1623+
1624+
// Helper: Extend a single ImGuiTextFilter to multiple fields. It tracks explicit pass/failure conditions and will
1625+
// stop processing text after an explicit failure.
1626+
// Usage:
1627+
// ImGuiTextFilterMatch match(filter);
1628+
// match.PassFilter(partial_text_to_match1);
1629+
// match.PassFilter(partial_text_to_match2);
1630+
// if (match) { ... }
1631+
struct ImGuiTextFilterMatch
1632+
{
1633+
ImGuiTextFilterMatch(const ImGuiTextFilter& filter);
1634+
1635+
IMGUI_API void PassFilter(const char* text, const char* text_end = NULL);
1636+
1637+
operator bool() const { return State == ImGuiTextFilterMatchResultPass; }
1638+
1639+
// [Internal]
1640+
const ImGuiTextFilter& Filter;
1641+
ImGuiTextFilterMatchResult State;
1642+
ImU64 MatchStates; // track which positive matches have been seen for And mode
1643+
int MatchCount;
1644+
int MatchMask;
16011645
};
16021646

16031647
// Helper: Growable text buffer for logging/accumulating text

0 commit comments

Comments
 (0)