Skip to content

Commit f8d1a10

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 ecb9b1e commit f8d1a10

File tree

2 files changed

+204
-33
lines changed

2 files changed

+204
-33
lines changed

imgui.cpp

Lines changed: 148 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2077,6 +2077,9 @@ void ImGuiStorage::SetAllInt(int v)
20772077

20782078
// Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]"
20792079
ImGuiTextFilter::ImGuiTextFilter(const char* default_filter)
2080+
: MatchMode(ImGuiTextFilterMode_Or)
2081+
, WordSplitter(',')
2082+
, MinWordSize(0)
20802083
{
20812084
if (default_filter)
20822085
{
@@ -2086,7 +2089,6 @@ ImGuiTextFilter::ImGuiTextFilter(const char* default_filter)
20862089
else
20872090
{
20882091
InputBuf[0] = 0;
2089-
CountGrep = 0;
20902092
}
20912093
}
20922094

@@ -2100,7 +2102,21 @@ bool ImGuiTextFilter::Draw(const char* label, float width)
21002102
return value_changed;
21012103
}
21022104

2103-
void ImGuiTextFilter::ImGuiTextRange::split(char separator, ImVector<ImGuiTextRange>* out) const
2105+
static void AddWordToTextRange(const char* begin, const char* end, ImVector<ImGuiTextFilter::ImGuiTextRange>* out, char minWordSize)
2106+
{
2107+
if (*begin == '-')
2108+
{
2109+
if ((end - begin) > minWordSize + 1)
2110+
out->push_front(ImGuiTextFilter::ImGuiTextRange(begin, end));
2111+
}
2112+
else
2113+
{
2114+
if ((end - begin) > minWordSize)
2115+
out->push_back(ImGuiTextFilter::ImGuiTextRange(begin, end));
2116+
}
2117+
}
2118+
2119+
void ImGuiTextFilter::ImGuiTextRange::split(char separator, ImVector<ImGuiTextRange>* out, char minWordSize) const
21042120
{
21052121
out->resize(0);
21062122
const char* wb = b;
@@ -2109,33 +2125,46 @@ void ImGuiTextFilter::ImGuiTextRange::split(char separator, ImVector<ImGuiTextRa
21092125
{
21102126
if (*we == separator)
21112127
{
2112-
out->push_back(ImGuiTextRange(wb, we));
2128+
AddWordToTextRange(wb, we, out, minWordSize);
21132129
wb = we + 1;
21142130
}
21152131
we++;
21162132
}
2117-
if (wb != we)
2118-
out->push_back(ImGuiTextRange(wb, we));
2133+
2134+
AddWordToTextRange(wb, we, out, minWordSize);
21192135
}
21202136

21212137
void ImGuiTextFilter::Build()
21222138
{
2139+
NegativeFilterCount = 0;
21232140
Filters.resize(0);
21242141
ImGuiTextRange input_range(InputBuf, InputBuf+strlen(InputBuf));
2125-
input_range.split(',', &Filters);
2142+
input_range.split(WordSplitter, &Filters, MinWordSize);
21262143

2127-
CountGrep = 0;
21282144
for (int i = 0; i != Filters.Size; i++)
21292145
{
21302146
ImGuiTextRange& f = Filters[i];
2147+
IM_ASSERT(!f.empty());
2148+
2149+
if (f.b[0] == '-')
2150+
{
2151+
IM_ASSERT(NegativeFilterCount == i);
2152+
NegativeFilterCount++;
2153+
f.b++;
2154+
}
2155+
21312156
while (f.b < f.e && ImCharIsBlankA(f.b[0]))
21322157
f.b++;
21332158
while (f.e > f.b && ImCharIsBlankA(f.e[-1]))
21342159
f.e--;
2160+
21352161
if (f.empty())
2136-
continue;
2137-
if (Filters[i].b[0] != '-')
2138-
CountGrep += 1;
2162+
{
2163+
Filters.erase(&f);
2164+
if (NegativeFilterCount == i)
2165+
NegativeFilterCount--;
2166+
i--;
2167+
}
21392168
}
21402169
}
21412170

@@ -2147,30 +2176,126 @@ bool ImGuiTextFilter::PassFilter(const char* text, const char* text_end) const
21472176
if (text == NULL)
21482177
text = "";
21492178

2150-
for (int i = 0; i != Filters.Size; i++)
2179+
int i = 0;
2180+
for (; i != NegativeFilterCount; i++)
21512181
{
21522182
const ImGuiTextRange& f = Filters[i];
2153-
if (f.empty())
2154-
continue;
2155-
if (f.b[0] == '-')
2183+
2184+
// Subtract
2185+
if (ImStristr(text, text_end, f.begin(), f.end()) != NULL)
2186+
return false;
2187+
}
2188+
2189+
// Implicit * grep
2190+
if (NegativeFilterCount == Filters.Size)
2191+
return true;
2192+
2193+
for (; i != Filters.Size; i++)
2194+
{
2195+
const ImGuiTextRange& f = Filters[i];
2196+
2197+
if (ImStristr(text, text_end, f.begin(), f.end()) != NULL)
21562198
{
2157-
// Subtract
2158-
if (ImStristr(text, text_end, f.b + 1, f.e) != NULL)
2159-
return false;
2199+
// in Or mode we stop testing after a single hit
2200+
if (MatchMode == ImGuiTextFilterMode_Or)
2201+
return true;
21602202
}
21612203
else
21622204
{
2163-
// Grep
2164-
if (ImStristr(text, text_end, f.b, f.e) != NULL)
2165-
return true;
2205+
// in And mode we stop testing after a single miss
2206+
if (MatchMode == ImGuiTextFilterMode_And)
2207+
return false;
2208+
}
2209+
}
2210+
2211+
return MatchMode == ImGuiTextFilterMode_And;
2212+
}
2213+
2214+
//-----------------------------------------------------------------------------
2215+
// [SECTION] ImGuiTextFilterMatch
2216+
//-----------------------------------------------------------------------------
2217+
ImGuiTextFilterMatch::ImGuiTextFilterMatch(const ImGuiTextFilter& filter)
2218+
: Filter(filter)
2219+
, State(ImGuiTextFilterMatchResultNone)
2220+
, MatchStates(0)
2221+
, MatchCount(filter.Filters.Size - filter.NegativeFilterCount)
2222+
, MatchMask((1 << MatchCount) - 1)
2223+
{
2224+
}
2225+
2226+
// duplicated so we don't add overhead to regular filters since it can add up quick when processing large amount of items
2227+
void ImGuiTextFilterMatch::PassFilter(const char* text, const char* text_end)
2228+
{
2229+
if (Filter.Filters.empty())
2230+
return;
2231+
2232+
// stop processing on any failure
2233+
if (State == ImGuiTextFilterMatchResultFail)
2234+
return;
2235+
2236+
// stop processing on any success with no negative filters
2237+
if (Filter.NegativeFilterCount == 0 && State == ImGuiTextFilterMatchResultPass)
2238+
return;
2239+
2240+
if (text == NULL)
2241+
text = "";
2242+
2243+
int i = 0;
2244+
for (; i != Filter.NegativeFilterCount; i++)
2245+
{
2246+
const ImGuiTextFilter::ImGuiTextRange& f = Filter.Filters[i];
2247+
2248+
// Subtract
2249+
if (ImStristr(text, text_end, f.begin(), f.end()) != NULL)
2250+
{
2251+
State = ImGuiTextFilterMatchResultFail;
2252+
return;
21662253
}
21672254
}
21682255

21692256
// Implicit * grep
2170-
if (CountGrep == 0)
2171-
return true;
2257+
if (Filter.NegativeFilterCount == Filter.Filters.Size)
2258+
{
2259+
State = ImGuiTextFilterMatchResultPass;
2260+
return;
2261+
}
21722262

2173-
return false;
2263+
// already passed all statements, no sense in continuing
2264+
if (State == ImGuiTextFilterMatchResultPass)
2265+
{
2266+
return;
2267+
}
2268+
2269+
for (; i != Filter.Filters.Size; i++)
2270+
{
2271+
const ImGuiTextFilter::ImGuiTextRange& f = Filter.Filters[i];
2272+
2273+
ImU64 test_bit = (1 << i) - Filter.NegativeFilterCount;
2274+
if ((MatchStates & test_bit) != 0)
2275+
{
2276+
continue;
2277+
}
2278+
2279+
if (ImStristr(text, text_end, f.begin(), f.end()) != NULL)
2280+
{
2281+
// in Or mode we stop testing after a single hit
2282+
if (Filter.MatchMode == ImGuiTextFilterMode_Or)
2283+
{
2284+
State = ImGuiTextFilterMatchResultPass;
2285+
return;
2286+
}
2287+
else
2288+
{
2289+
MatchStates |= test_bit;
2290+
}
2291+
}
2292+
}
2293+
2294+
// Check if all bits are set after each check
2295+
if (MatchStates == MatchMask)
2296+
{
2297+
State = ImGuiTextFilterMatchResultPass;
2298+
}
21742299
}
21752300

21762301
//-----------------------------------------------------------------------------

imgui.h

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

121122
// 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)
122123
// Use your programming IDE "Go to definition" facility on the names of the center columns to find the actual flags/enum lists.
@@ -133,6 +134,8 @@ typedef int ImGuiKey; // -> enum ImGuiKey_ // Enum: A
133134
typedef int ImGuiNavInput; // -> enum ImGuiNavInput_ // Enum: An input identifier for navigation
134135
typedef int ImGuiMouseCursor; // -> enum ImGuiMouseCursor_ // Enum: A mouse cursor identifier
135136
typedef int ImGuiStyleVar; // -> enum ImGuiStyleVar_ // Enum: A variable identifier for styling
137+
typedef int ImGuiTextFilterMode; // -> enum ImGuiTextFilterMode_ // Enum: To control how text filter handles multiple words
138+
typedef int ImGuiTextFilterMatchResult; // -> enum ImGuiTextFilterMatchResult_ // Enum: A match identifier to better control applying a text filter over multiple fields
136139
typedef int ImDrawCornerFlags; // -> enum ImDrawCornerFlags_ // Flags: for ImDrawList::AddRect(), AddRectFilled() etc.
137140
typedef int ImDrawListFlags; // -> enum ImDrawListFlags_ // Flags: for ImDrawList
138141
typedef int ImFontAtlasFlags; // -> enum ImFontAtlasFlags_ // Flags: for ImFontAtlas
@@ -1582,6 +1585,12 @@ struct ImGuiOnceUponAFrame
15821585
operator bool() const { int current_frame = ImGui::GetFrameCount(); if (RefFrame == current_frame) return false; RefFrame = current_frame; return true; }
15831586
};
15841587

1588+
enum ImGuiTextFilterMode_
1589+
{
1590+
ImGuiTextFilterMode_Or, // A single word match will pass the filter
1591+
ImGuiTextFilterMode_And // All words must match to pass the filter
1592+
};
1593+
15851594
// Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]"
15861595
struct ImGuiTextFilter
15871596
{
@@ -1595,17 +1604,54 @@ struct ImGuiTextFilter
15951604
// [Internal]
15961605
struct ImGuiTextRange
15971606
{
1598-
const char* b;
1599-
const char* e;
1600-
1601-
ImGuiTextRange() { b = e = NULL; }
1602-
ImGuiTextRange(const char* _b, const char* _e) { b = _b; e = _e; }
1603-
bool empty() const { return b == e; }
1604-
IMGUI_API void split(char separator, ImVector<ImGuiTextRange>* out) const;
1607+
const char* b;
1608+
const char* e;
1609+
1610+
ImGuiTextRange() { b = e = NULL; }
1611+
ImGuiTextRange(const char* _b, const char* _e) { b = _b; e = _e; }
1612+
const char* begin() const { return b; }
1613+
const char* end () const { return e; }
1614+
bool empty() const { return b == e; }
1615+
IMGUI_API void split(char separator, ImVector<ImGuiTextRange>* out, char minWordSize = 0) const;
16051616
};
1606-
char InputBuf[256];
1607-
ImVector<ImGuiTextRange>Filters;
1608-
int CountGrep;
1617+
char InputBuf[256];
1618+
ImVector<ImGuiTextRange> Filters;
1619+
int NegativeFilterCount;
1620+
1621+
// [Configuration]
1622+
ImGuiTextFilterMode MatchMode; // if true then all words must match, otherwise any matching word will be a pass
1623+
char WordSplitter; // Character used to split user string, ',' by default
1624+
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
1625+
};
1626+
1627+
enum ImGuiTextFilterMatchResult_
1628+
{
1629+
ImGuiTextFilterMatchResultNone, // There have been no matches or failures
1630+
ImGuiTextFilterMatchResultFail, // The match explicitly failed because of a subtractive clause or because no positive matches passed
1631+
ImGuiTextFilterMatchResultPass, // The match explicitly passed because of positive match
1632+
};
1633+
1634+
// Helper: Extend a single ImGuiTextFilter to multiple fields. It tracks explicit pass/failure conditions and will
1635+
// stop processing text after an explicit failure.
1636+
// Usage:
1637+
// ImGuiTextFilterMatch match(filter);
1638+
// match.PassFilter(partial_text_to_match1);
1639+
// match.PassFilter(partial_text_to_match2);
1640+
// if (match) { ... }
1641+
struct ImGuiTextFilterMatch
1642+
{
1643+
ImGuiTextFilterMatch(const ImGuiTextFilter& filter);
1644+
1645+
IMGUI_API void PassFilter(const char* text, const char* text_end = NULL);
1646+
1647+
operator bool() const { return State == ImGuiTextFilterMatchResultPass; }
1648+
1649+
// [Internal]
1650+
const ImGuiTextFilter& Filter;
1651+
ImGuiTextFilterMatchResult State;
1652+
ImU64 MatchStates; // track which positive matches have been seen for And mode
1653+
int MatchCount;
1654+
int MatchMask;
16091655
};
16101656

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

0 commit comments

Comments
 (0)