diff --git a/src/ui/windows/console_output_model.cpp b/src/ui/windows/console_output_model.cpp index bac5550..e4a955f 100644 --- a/src/ui/windows/console_output_model.cpp +++ b/src/ui/windows/console_output_model.cpp @@ -6,13 +6,18 @@ namespace dragonx { namespace ui { -namespace { -std::string lowerCopy(std::string value) +size_t FindCaseInsensitive(const char* hay, size_t hayLen, const std::string& needleLower, size_t from) { - std::transform(value.begin(), value.end(), value.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - return value; -} + const size_t n = needleLower.size(); + if (n == 0) return from <= hayLen ? from : std::string::npos; + if (hayLen < n) return std::string::npos; + auto fold = [](unsigned char c) { return (c >= 'A' && c <= 'Z') ? static_cast(c + 32) : c; }; + for (size_t i = from; i + n <= hayLen; ++i) { + size_t j = 0; + while (j < n && fold(static_cast(hay[i + j])) == fold(static_cast(needleLower[j]))) ++j; + if (j == n) return i; + } + return std::string::npos; } bool consoleLinePassesFilter(const std::string& lineText, @@ -25,9 +30,7 @@ bool consoleLinePassesFilter(const std::string& lineText, if (!filter.appMessagesEnabled && channel == ConsoleChannel::App) return false; if (filter.errorsOnly && channel != ConsoleChannel::Error) return false; if (!filter.text.empty()) { - std::string needle = lowerCopy(filter.text); - std::string haystack = lowerCopy(lineText); - if (haystack.find(needle) == std::string::npos) return false; + if (FindCaseInsensitive(lineText.data(), lineText.size(), filter.text, 0) == std::string::npos) return false; } return true; } diff --git a/src/ui/windows/console_output_model.h b/src/ui/windows/console_output_model.h index 121744a..add8ff9 100644 --- a/src/ui/windows/console_output_model.h +++ b/src/ui/windows/console_output_model.h @@ -18,6 +18,10 @@ struct ConsoleOutputFilter { bool appMessagesEnabled = true; // App channel }; +// Alloc-free ASCII case-insensitive substring search (C-locale fold). Returns the byte +// offset of the first match at or after `from`, or npos. Needle is folded on the fly. +size_t FindCaseInsensitive(const char* hay, size_t hayLen, const std::string& needle, size_t from); + // Whether a line with the given channel + text passes the active filter. bool consoleLinePassesFilter(const std::string& lineText, ConsoleChannel channel, diff --git a/src/ui/windows/console_tab.cpp b/src/ui/windows/console_tab.cpp index b025800..fb1c253 100644 --- a/src/ui/windows/console_tab.cpp +++ b/src/ui/windows/console_tab.cpp @@ -1196,11 +1196,9 @@ void ConsoleTab::drawVisibleLines(float padX, float lineHeight, bool hasTextFilt // Filter match highlight (case-insensitive) within this segment. if (hasTextFilter && !filterLower.empty() && seg.byteStart < seg.byteEnd) { - std::string segLower(segStart, segEnd); - std::transform(segLower.begin(), segLower.end(), segLower.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); + const size_t segBytes = static_cast(segEnd - segStart); size_t pos = 0; - while ((pos = segLower.find(filterLower, pos)) != std::string::npos) { + while ((pos = FindCaseInsensitive(segStart, segBytes, filterLower, pos)) != std::string::npos) { float xs = font->CalcTextSizeA(fontSize, FLT_MAX, 0, segStart, segStart + static_cast(pos)).x; float xe = font->CalcTextSizeA(fontSize, FLT_MAX, 0, segStart, segStart + static_cast(pos + filterLower.size())).x;