ImGuiTextFilter: reworked filter to make space an AND operator. Demo: tweaked demo.

Removed FilterOp, MinWordSize added a few commits ago.
This commit is contained in:
ocornut 2026-09-21 14:16:55 +02:00
parent 420f179341
commit d5c68c301a
4 changed files with 73 additions and 48 deletions

View file

@ -84,8 +84,10 @@ Other Changes:
hasn't been owned by another items. This is more correct and also necessary
to avoid Drags/Sliders cancelling also triggering a cancel menu. (#8564, #9534)
- ImGuiTextFilter:
- Added support for space ' ' as separator, which is more standard. (#2435, #30)
- Added support for quoted "xxx" blocks to e.g. match words sequences. (#2435)
- Added support for space as an "and" filter, which is the standard. (#2435, #30)
e.g. w/ filter 'hello world' (without quotes) both words needs to be in input data.
- Added support for double-quoted "xxx" blocks to e.g. match words sequences. (#2435)
e.g. w/ filter "hello world" the exact sequence needs to be in input data.
- Fixed filtering when mixing "include" and "-exclude" keyword so that an "-exclude"
match always filters out. (#2435) [@kudaba, @ocornut]
- Added `DrawWithHint()` function. (#6206, #6395, #6447)

View file

@ -3094,9 +3094,7 @@ IM_MSVC_RUNTIME_CHECKS_RESTORE
ImGuiTextFilter::ImGuiTextFilter(const char* default_filter) //-V1077
{
InputBuf[0] = 0;
FilterOp = '|';
MinWordSize = 1;
_CountExclude = _CountInclude = 0;
_CountExclude = 0;
if (default_filter)
{
ImStrncpy(InputBuf, default_filter, IM_COUNTOF(InputBuf));
@ -3118,14 +3116,15 @@ bool ImGuiTextFilter::DrawWithHint(const char* label, const char* hint)
return value_changed;
}
// Parse filter and split into items
// Parse filter and split into a format optimal for PassFilter()
void ImGuiTextFilter::Build()
{
_Items.resize(0);
_CountExclude = _CountInclude = 0;
IM_ASSERT(FilterOp == '|' || FilterOp == '&');
_CountExclude = 0;
const char* buf_e = InputBuf + ImStrlen(InputBuf);
const char* word_e;
int seq_incl_start_idx = -1;
for (const char* word_b = InputBuf; word_b < buf_e; word_b = word_e + 1)
{
// Trim blanks
@ -3151,20 +3150,33 @@ void ImGuiTextFilter::Build()
}
// Min length
if (word_e - word_b < MinWordSize)
continue;
// Add to list
// The '-' is not stored in items but implicitly inferred using (n < CountExclude).
// FIXME-OPT: about ~push_front(): as N is derived from user inputs we expect this to be fine.
_Items.insert(is_excl ? _Items.Data : _Items.Data + _Items.Size, ImGuiTextFilter::ImGuiTextFilterItem(word_b, word_e));
if (word_e - word_b > 0)
{
// Add to list. The '-' is not stored in items but implicitly inferred using (n < CountExclude).
// FIXME-OPT: as items are derived from user input buffer and we expect the insert() to behave sanely.
if (is_excl)
{
_Items.insert(_Items.Data + _CountExclude, ImGuiTextFilter::ImGuiTextFilterItem(word_b, word_e));
_CountExclude++;
if (seq_incl_start_idx != -1)
seq_incl_start_idx++;
}
else
_CountInclude++;
{
if (seq_incl_start_idx == -1)
seq_incl_start_idx = _Items.Size;
_Items.insert(_Items.Data + _Items.Size, ImGuiTextFilter::ImGuiTextFilterItem(word_b, word_e));
_Items.Data[seq_incl_start_idx].CountInclude++;
}
}
// Next sequence
if (word_e[0] == ',')
seq_incl_start_idx = -1;
}
}
// FIXME: Could use a specialized ImStristr() + pre-convert our filter to uppercase.
bool ImGuiTextFilter::PassFilter(const char* text, const char* text_end) const
{
if (_Items.Size == 0)
@ -3173,24 +3185,28 @@ bool ImGuiTextFilter::PassFilter(const char* text, const char* text_end) const
text = text_end = "";
// Filters are sorted so that '-' ones are always leading.
int n;
for (n = 0; n < _CountExclude; n++)
if (ImStristr(text, text_end, _Items.Data[n].Begin, _Items.Data[n].End) != NULL)
ImGuiTextFilterItem* seq = _Items.Data;
ImGuiTextFilterItem* seq_excl_end = _Items.Data + _CountExclude;
for (; seq < seq_excl_end; seq++)
if (ImStristr(text, text_end, seq->Begin, seq->Begin + seq->Len) != NULL)
return false;
const bool is_and_filter = (FilterOp == '&');
for (; n < _Items.Size; n++)
{
const bool is_match = ImStristr(text, text_end, _Items.Data[n].Begin, _Items.Data[n].End) != NULL;
if (is_match && !is_and_filter) // or incl 1 -> true
return true; // or incl 0 -> continue
if (!is_match && is_and_filter) // and incl 1 -> continue
return false; // and incl 0 -> false
}
// When no inclusion are specified (only exclusions) we implicitly pass
if (_CountInclude == 0)
// Process includes
ImGuiTextFilterItem* seq_incl_end = _Items.Data + _Items.Size;
if (seq == seq_incl_end) // When no inclusion are specified (only exclusions) we implicitly pass
return true;
return is_and_filter;
while (seq < seq_incl_end)
{
ImGuiTextFilterItem* seq_next = seq + seq->CountInclude;
IM_ASSERT_PARANOID(seq->CountInclude > 0 && seq_next <= seq_incl_end);
for (; seq < seq_next; seq++)
if (ImStristr(text, text_end, seq->Begin, seq->Begin + seq->Len) == NULL)
break;
if (seq == seq_next) // All matched
return true;
seq = seq_next; // Try next
}
return false;
}
//-----------------------------------------------------------------------------

14
imgui.h
View file

@ -2785,7 +2785,7 @@ struct ImGuiOnceUponAFrame
operator bool() const { int current_frame = ImGui::GetFrameCount(); if (RefFrame == current_frame) return false; RefFrame = current_frame; return true; }
};
// Helper: Parse and apply text filters e.g. 'aaa bbb -ccc'.
// Helper: Parse and apply text filters e.g. 'aaa bbb' (all), 'aaa,bbb' (any), '-aaa' (exclude), '"Hello, world"' (exact sequence)
struct ImGuiTextFilter
{
IMGUI_API ImGuiTextFilter(const char* default_filter = "");
@ -2801,20 +2801,18 @@ struct ImGuiTextFilter
inline bool Draw(const char* label, float width) { if (width != 0.0f) ImGui::SetNextItemWidth(width); return Draw(label); }
#endif
// [Internal] Don't use! Will be replaced with ImStrv.
// [Internal] Types
struct ImGuiTextFilterItem
{
const char* Begin;
const char* End;
ImGuiTextFilterItem(const char* b, const char* e) { Begin = b; End = e; }
int Len;
int CountInclude; // >0 when beginning of an AND chain.
ImGuiTextFilterItem(const char* b, const char* e) { IM_ASSERT(e > b); Begin = b; Len = (int)(e - b); CountInclude = 0; }
};
// [Internal] Members
char InputBuf[256]; // User input buffer
char FilterOp; // == '|' (any) pr '&' (all)
ImU8 MinWordSize; // == 1
int _CountExclude; // >= 0
int _CountInclude; // >= 0
int _CountExclude; // >= 0 count of leading exclude
ImVector<ImGuiTextFilterItem> _Items; // Pre-parsed, trimmed, reordered items
};

View file

@ -3943,15 +3943,24 @@ static void DemoWindowWidgetsTextFilter()
static ImGuiTextFilter filter;
ImGui::Text("Filter usage:\n"
" \"\" display all lines\n"
" \"xxx\" display lines containing \"xxx\"\n"
" \"xxx,yyy\" display lines containing \"xxx\" or \"yyy\"\n"
" \"-xxx\" hide lines containing \"xxx\"");
" xxx display lines containing \"xxx\"\n"
" xxx yyy display lines containing \"xxx\" and \"yyy\"\n"
" \"xxx yyy\" display lines containing \"xxx yyy\"\n"
" xxx,yyy display lines containing \"xxx\" or \"yyy\"\n"
" -xxx hide lines containing \"xxx\"");
ImGui::SetNextItemWidth(-FLT_MIN);
filter.DrawWithHint("##Filter", "Filter (incl -excl)");
if (ImGui::BeginChild("##items", ImVec2(-FLT_MIN, ImGui::GetTextLineHeightWithSpacing() * 15), ImGuiChildFlags_FrameStyle))
{
const char* lines[] = { "aaa1.c", "bbb1.c", "ccc1.c", "aaa2.cpp", "bbb2.cpp", "ccc2.cpp", "abc.h", "hello, world" };
for (int i = 0; i < IM_COUNTOF(lines); i++)
if (filter.PassFilter(lines[i]))
ImGui::BulletText("%s", lines[i]);
for (const char* item : lines)
if (filter.PassFilter(item))
ImGui::TextUnformatted(item);
for (const char* item : ExampleNames)
if (filter.PassFilter(item))
ImGui::TextUnformatted(item);
}
ImGui::EndChild();
ImGui::TreePop();
}
}