perf(send): poll the clipboard for the paste hint only when it helps

The send tab read the OS clipboard every frame the paste button was
hovered, to preview a pasted address. SDL's clipboard read is a
synchronous round-trip that can stall, and the preview can only ever
latch when the recipient field is empty, so most of those reads were
wasted work.

- The clipboard is now read only while the button is hovered and the
  recipient field is empty, polled once on hover entry and then at most
  twice a second.
- Trimming the clipboard text moves to a shared helper that also bounds
  the copy so a pathological multi-megabyte clipboard is ignored.

Tests: the trim helper handles null, surrounding whitespace, an
all-whitespace string, and the length bound.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 16:00:10 -05:00
co-authored by Claude Opus 4.8
parent 3f82af99c4
commit 76daf9619d
4 changed files with 37 additions and 15 deletions
+14 -15
View File
@@ -1416,21 +1416,20 @@ void RenderSendTab(App* app)
paste_hovered = material::IsRectHovered(pasteBtnPos,
ImVec2(pasteBtnPos.x + pasteBtnSize.x, pasteBtnPos.y + pasteBtnSize.y));
// Handle preview state — don't modify s_to_address, just store preview text
if (paste_hovered && !s_paste_previewing) {
const char* clip = ImGui::GetClipboardText();
if (clip && clip[0] != '\0') {
std::string trimmed(clip);
while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\n' || trimmed.front() == '\r' || trimmed.front() == '\t'))
trimmed.erase(trimmed.begin());
while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\n' || trimmed.back() == '\r' || trimmed.back() == '\t'))
trimmed.pop_back();
// Preview any address, a mining one too: once pasted, the field explains why it can't be paid.
bool looksValid = dragonx::util::isDragonXAddress(trimmed);
if (looksValid && s_to_address[0] == '\0') {
s_preview_text = trimmed;
s_paste_previewing = true;
}
// Handle preview state — don't modify s_to_address, just store preview text.
// The OS clipboard is only worth reading when the result can latch (field empty), polled on hover entry
// and then at most every 0.5 s (SDL's clipboard is a synchronous round-trip: 1-5 ms on X11, can stall).
static double s_nextClipPoll = 0.0;
if (!paste_hovered) s_nextClipPoll = 0.0; // poll immediately on the next hover entry
if (paste_hovered && !s_paste_previewing && s_to_address[0] == '\0' &&
ImGui::GetTime() >= s_nextClipPoll) {
s_nextClipPoll = ImGui::GetTime() + 0.5;
// Copy immediately: the backend frees the previous clipboard buffer on the next call.
const std::string trimmed = dragonx::util::trimmedClipboardText(ImGui::GetClipboardText());
// Preview any address, a mining one too: once pasted, the field explains why it can't be paid.
if (!trimmed.empty() && dragonx::util::isDragonXAddress(trimmed)) {
s_preview_text = trimmed;
s_paste_previewing = true;
}
} else if (!paste_hovered && s_paste_previewing) {
s_paste_previewing = false;
+13
View File
@@ -118,5 +118,18 @@ bool containsIgnoreCase(std::string_view haystack, std::string_view needle)
return false;
}
std::string trimmedClipboardText(const char* clip, std::size_t maxBytes)
{
if (!clip || clip[0] == '\0') return {};
std::size_t n = 0;
while (n <= maxBytes && clip[n] != '\0') ++n;
if (n > maxBytes) return {};
const auto ws = [](char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; };
std::size_t b = 0, e = n;
while (b < e && ws(clip[b])) ++b;
while (e > b && ws(clip[e - 1])) --e;
return std::string(clip + b, e - b);
}
} // namespace util
} // namespace dragonx
+4
View File
@@ -82,5 +82,9 @@ std::string truncateMiddleToFit(const std::string& s, int minFront, int minBack,
return build(lo);
}
// Clipboard text -> trimmed (ASCII whitespace ' ', \t, \n, \r) candidate string. Empty for a null/empty clip or one
// longer than maxBytes (an address is < 100 bytes; this bounds the copy of a pathological multi-MB clipboard).
std::string trimmedClipboardText(const char* clip, std::size_t maxBytes = 1024);
} // namespace util
} // namespace dragonx