diff --git a/src/ui/windows/console_tab.cpp b/src/ui/windows/console_tab.cpp index b031e1a..25b5a29 100644 --- a/src/ui/windows/console_tab.cpp +++ b/src/ui/windows/console_tab.cpp @@ -553,6 +553,12 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec) float filterW = std::min(schema::UI().drawElement("tabs.console", "filter-max-width").size, filterAvail * schema::UI().drawElement("tabs.console", "filter-width-ratio").size); ImGui::SetNextItemWidth(filterW); ImGui::InputTextWithHint("##ConsoleFilter", TR("console_filter_hint"), filter_text_, sizeof(filter_text_)); + if (filter_text_[0] != '\0') { + ImGui::SameLine(0, Layout::spacingSm()); + std::string mc = std::to_string(filter_match_count_) + " " + TR("console_matches"); + Type().textColored(TypeStyle::Caption, + filter_match_count_ > 0 ? OnSurfaceMedium() : Error(), mc.c_str()); + } // Zoom +/- buttons (right side of toolbar) ImGui::SameLine(); @@ -612,6 +618,13 @@ void ConsoleTab::renderOutput() s_app_messages_enabled, COLOR_DAEMON, COLOR_ERROR, COLOR_RPC}; bool has_text_filter = !outputFilter.text.empty(); + // Lowercased needle for in-line match highlighting. + std::string filter_lower; + if (has_text_filter) { + filter_lower = std::string(filter_text_); + std::transform(filter_lower.begin(), filter_lower.end(), filter_lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + } bool has_filter = has_text_filter || !outputFilter.daemonMessagesEnabled || !outputFilter.rpcTraceEnabled || !outputFilter.appMessagesEnabled || outputFilter.errorsOnly; @@ -628,6 +641,7 @@ void ConsoleTab::renderOutput() } } int visible_count = static_cast(visible_indices_.size()); + filter_match_count_ = has_text_filter ? visible_count : 0; // Calculate wrapped heights AND build sub-row segments for each visible line. // Each segment records which bytes of the source text appear on that visual @@ -881,6 +895,23 @@ void ConsoleTab::renderOutput() selColor); } + // Filter match highlight (case-insensitive) within this segment. + if (has_text_filter && !filter_lower.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)); }); + size_t pos = 0; + while ((pos = segLower.find(filter_lower, 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 + filter_lower.size())).x; + dl->AddRectFilled(ImVec2(lineOrigin.x + xs, rowY), + ImVec2(lineOrigin.x + xe, rowY + seg.height), + IM_COL32(255, 210, 0, 70)); + pos += filter_lower.size(); + } + } + // Render text segment if (seg.byteStart < seg.byteEnd) { dl->AddText(font, fontSize, diff --git a/src/ui/windows/console_tab.h b/src/ui/windows/console_tab.h index 9f0a7c6..78e7270 100644 --- a/src/ui/windows/console_tab.h +++ b/src/ui/windows/console_tab.h @@ -152,6 +152,7 @@ private: // Output filter char filter_text_[128] = {0}; + mutable int filter_match_count_ = 0; // lines matching the text filter (for the toolbar) mutable std::vector visible_indices_; // Cached for selection mapping // Wrapped line height caching (for variable-height text wrapping) diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 834d39c..de87c86 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -983,6 +983,7 @@ void I18n::loadBuiltinEnglish() strings_["console_help_stop"] = " stop - Stop the daemon"; strings_["console_line_count"] = "%zu lines"; strings_["console_new_lines"] = "new"; + strings_["console_matches"] = "matches"; strings_["console_new_lines"] = "%d new lines"; strings_["console_no_daemon"] = "No daemon"; strings_["console_not_connected"] = "Error: Not connected to daemon";