Files
ObsidianDragon/src/ui/windows/balance_tab_helpers.cpp
DanS e40962cdf2 perf(ui): dedupe time-ago + allocation-free case-insensitive filter (audit #1-3)
Per-frame hot paths in the immediate-mode UI were allocating needlessly:

- Address filtering in the balance tab rebuilt a std::string filter per address AND
  containsIgnoreCase() lower-cased two fresh copies per call — ~6×N allocations/frame
  on large wallets. New util::containsIgnoreCase(string_view, string_view) is
  allocation-free, and the filter is now built once outside the loop.
- Four duplicated "time ago" implementations (balance_tab_helpers, balance_recent_tx,
  send_tab, transactions_tab) are consolidated into util::formatTimeAgo (localized long
  form) + util::formatTimeAgoShort (compact "5s ago"), preserving each call site's exact
  display style. Both use snprintf, no per-row string concatenation.
- The send-tab address-suggestion scan (a walk over the whole tx list) is memoized on the
  typed text + tx count, so it no longer recomputes every frame while the user pauses.

New src/util/text_format.{h,cpp}; the two existing containsIgnoreCase/timeAgo definitions
now delegate to it. Added to both the app and test targets (test target also gains i18n.cpp,
which text_format's localized path needs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 13:54:27 -05:00

108 lines
3.2 KiB
C++

#include "balance_tab_helpers.h"
#include "../../config/version.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "../../util/i18n.h"
#include "../../util/text_format.h"
#include "../material/colors.h"
#include "../material/type.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <ctime>
namespace dragonx {
namespace ui {
std::string TrId(const char* trKey, const char* id)
{
std::string value = TR(trKey);
value += "##";
value += id;
return value;
}
// Thin wrappers over the shared, allocation-free helpers (kept for existing call sites).
bool containsIgnoreCase(const std::string& value, const std::string& search)
{
return util::containsIgnoreCase(value, search);
}
std::string timeAgo(int64_t timestamp)
{
return util::formatTimeAgoShort(timestamp);
}
std::string truncateAddress(const std::string& address, int maxLen)
{
if (maxLen <= 3 || address.length() <= static_cast<size_t>(maxLen)) return address;
int half = (maxLen - 3) / 2;
return address.substr(0, half) + "..." + address.substr(address.length() - half);
}
ImU32 recentTxIconColor(const std::string& type)
{
using namespace material;
if (type == "send") return Error();
if (type == "receive") return Success();
return Warning();
}
ImU32 recentTxAmountColor(const std::string& type)
{
using namespace material;
return type == "send" ? Error() : Success();
}
std::string formatRecentTxAmount(const std::string& type, double amount)
{
char buffer[32];
snprintf(buffer, sizeof(buffer), "%s%.4f %s",
type == "send" ? "-" : "+",
std::abs(amount), DRAGONX_TICKER);
return std::string(buffer);
}
void DrawTxIcon(ImDrawList* drawList, const std::string& type,
float centerX, float centerY, float, ImU32 color)
{
using namespace material;
ImFont* iconFont = Type().iconSmall();
const char* icon = ICON_MD_CONSTRUCTION;
if (type == "send") {
icon = ICON_MD_CALL_MADE;
} else if (type == "receive") {
icon = ICON_MD_CALL_RECEIVED;
}
ImVec2 size = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon);
drawList->AddText(iconFont, iconFont->LegacySize,
ImVec2(centerX - size.x * 0.5f, centerY - size.y * 0.5f), color, icon);
}
void DrawSparkline(ImDrawList* drawList, const ImVec2& min, const ImVec2& max,
const std::vector<double>& data, ImU32 color, float thickness)
{
if (data.size() < 2) return;
double low = *std::min_element(data.begin(), data.end());
double high = *std::max_element(data.begin(), data.end());
double range = high - low;
if (range < 1e-12) range = 1.0;
float width = max.x - min.x;
float height = max.y - min.y;
int count = static_cast<int>(data.size());
std::vector<ImVec2> points;
points.reserve(count);
for (int i = 0; i < count; i++) {
float x = min.x + static_cast<float>(i) / static_cast<float>(count - 1) * width;
float y = max.y - static_cast<float>((data[i] - low) / range) * height;
points.push_back(ImVec2(x, y));
}
drawList->AddPolyline(points.data(), count, color, ImDrawFlags_None, thickness);
}
} // namespace ui
} // namespace dragonx