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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user