feat(explorer): live (debounced) search as the user types

The explorer search only ran on Enter or the Search button. Now it also fires
automatically ~350ms after the user stops typing, once the query is resolvable
— a block height (all digits) or a complete 64-char hash/txid. Partial hex is
ignored so it won't flash "invalid query" mid-type, per-keystroke RPC spam is
avoided via the debounce, and the same query isn't re-run. Enter/button still
work for an immediate search.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 12:07:01 -05:00
parent 101c835c46
commit 09ab8d52c5

View File

@@ -41,6 +41,20 @@ using namespace material;
static char s_search_buf[128] = {};
static bool s_search_loading = false;
static std::string s_search_error;
static double s_search_change_time = -1.0; // GetTime() of the last edit (debounce for live search)
static std::string s_search_last_query; // last query actually run (avoids re-running it)
static constexpr double kSearchDebounceSeconds = 0.35;
// A query that performSearch() can resolve to a concrete result: a block height (all digits) or a
// complete 64-char block hash / txid. Partial hex is intentionally NOT resolvable, so live search
// stays quiet (no "invalid query") until the user finishes typing or pasting a full hash.
static bool isResolvableQuery(const std::string& q) {
if (q.empty()) return false;
if (std::all_of(q.begin(), q.end(), ::isdigit)) return true;
return q.size() == 64 && std::all_of(q.begin(), q.end(), [](char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
});
}
using BlockSummary = ExplorerBlockSummary;
static ExplorerBlockCache s_recent_block_cache;
@@ -462,6 +476,7 @@ static void renderSearchBar(App* app, float availWidth) {
ImGui::SetNextItemWidth(inputW);
bool enter = ImGui::InputText("##ExplorerSearch", s_search_buf, sizeof(s_search_buf),
ImGuiInputTextFlags_EnterReturnsTrue);
bool edited = ImGui::IsItemEdited(); // must read immediately after the input item
ImGui::PopStyleVar();
ImGui::SameLine();
@@ -471,10 +486,26 @@ static void renderSearchBar(App* app, float availWidth) {
bool clicked = material::StyledButton(TR("explorer_search"),
ImVec2(btnW, barH));
if ((enter || clicked) && app->rpc() && app->rpc()->isConnected()) {
std::string query(s_search_buf);
while (!query.empty() && query.front() == ' ') query.erase(query.begin());
while (!query.empty() && query.back() == ' ') query.pop_back();
std::string query(s_search_buf);
while (!query.empty() && query.front() == ' ') query.erase(query.begin());
while (!query.empty() && query.back() == ' ') query.pop_back();
if (edited) {
s_search_change_time = ImGui::GetTime(); // (re)start the debounce on each keystroke
s_search_error.clear(); // don't show a stale error mid-type
}
bool doSearch = enter || clicked;
// Live search: once the user pauses and the query is resolvable, run it without waiting for Enter.
if (!doSearch && s_search_change_time >= 0.0 &&
(ImGui::GetTime() - s_search_change_time) >= kSearchDebounceSeconds &&
isResolvableQuery(query) && query != s_search_last_query) {
doSearch = true;
}
if (doSearch && app->rpc() && app->rpc()->isConnected() && !query.empty()) {
s_search_change_time = -1.0; // consume the debounce
s_search_last_query = query;
performSearch(app, query);
}