From d5c68c301a3d564e54051a7a78ee7858c4b821fb Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 21 Sep 2026 14:16:55 +0200 Subject: [PATCH] ImGuiTextFilter: reworked filter to make space an AND operator. Demo: tweaked demo. Removed FilterOp, MinWordSize added a few commits ago. --- docs/CHANGELOG.txt | 6 ++-- imgui.cpp | 78 ++++++++++++++++++++++++++++------------------ imgui.h | 14 ++++----- imgui_demo.cpp | 23 +++++++++----- 4 files changed, 73 insertions(+), 48 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 82c2ffe00..ca1e27e49 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -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) diff --git a/imgui.cpp b/imgui.cpp index f9dc89e53..7e353df8f 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -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; + 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 + { + 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++; + } + } - // 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 (is_excl) - _CountExclude++; - else - _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; } //----------------------------------------------------------------------------- diff --git a/imgui.h b/imgui.h index 2ad3b91fb..1e0ce7dcc 100644 --- a/imgui.h +++ b/imgui.h @@ -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 _Items; // Pre-parsed, trimmed, reordered items }; diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 5ef47f3af..0b16dab3c 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -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)"); - 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]); + 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 (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(); } }