diff --git a/imgui.cpp b/imgui.cpp index 715b5dbb4122..8195b6c84791 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -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) { @@ -2053,7 +2056,6 @@ ImGuiTextFilter::ImGuiTextFilter(const char* default_filter) else { InputBuf[0] = 0; - CountGrep = 0; } } @@ -2067,7 +2069,21 @@ bool ImGuiTextFilter::Draw(const char* label, float width) return value_changed; } -void ImGuiTextFilter::ImGuiTextRange::split(char separator, ImVector* out) const +static void AddWordToTextRange(const char* begin, const char* end, ImVector* 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* out, char minWordSize) const { out->resize(0); const char* wb = b; @@ -2076,33 +2092,46 @@ void ImGuiTextFilter::ImGuiTextRange::split(char separator, ImVectorpush_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--; + } } } @@ -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; + } } //----------------------------------------------------------------------------- diff --git a/imgui.h b/imgui.h index 788f74c9c797..220458a5cfb5 100644 --- a/imgui.h +++ b/imgui.h @@ -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! @@ -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 @@ -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 { @@ -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* 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* out, char minWordSize = 0) const; }; - char InputBuf[256]; - ImVectorFilters; - int CountGrep; + char InputBuf[256]; + ImVector 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