The inline detail panel was tracked by the filtered list index. The display list is rebuilt and re-sorted on every data change, sort switch or filter edit, so a stored index pointed at a different transaction after the rebuild, and a page reset was triggered by any change in the row count, bouncing the view back to page 1. - The open row is now tracked by a stable content key (txid, display type, address and the amount's exact bits) so the right row stays open across rebuilds. - The page resets and the open row collapses only when a filter input changes (type, sort or the full search text). A new or removed row is data and now just clamps the current page. - The open row is also cleared on page navigation and when the list empties, so a wallet switch does not resurrect an old expansion. Adds pure helpers rowKey/filterInputsChanged to transactions_tab_helpers.h. Tests: rowKey distinguishes each field and separates +0.0 from -0.0, and filterInputsChanged latches each input change but not the first frame. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
92 lines
3.3 KiB
C++
92 lines
3.3 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
#pragma once
|
|
// Pure helpers behind the History tab (no ImGui), so they can be unit-tested.
|
|
#include "../../data/wallet_state.h"
|
|
#include <cstddef>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
namespace history {
|
|
|
|
// Rows of one txid chained in ascending index order. first[i] = lowest index sharing tx i's txid;
|
|
// next[i] = next higher index with the same txid, or -1. Holds indices only (no string copies, no
|
|
// pointers into the list), so it stays valid for as long as the list is unchanged (same tx_revision).
|
|
struct TxidGroups {
|
|
std::vector<int> first;
|
|
std::vector<int> next;
|
|
bool hasSiblings(std::size_t i) const { return next[static_cast<std::size_t>(first[i])] >= 0; }
|
|
};
|
|
|
|
inline TxidGroups groupByTxid(const std::vector<TransactionInfo>& txs)
|
|
{
|
|
TxidGroups g;
|
|
const std::size_t n = txs.size();
|
|
g.first.assign(n, -1);
|
|
g.next.assign(n, -1);
|
|
std::unordered_map<std::string_view, int> last; // txid (view into txs) -> last index seen
|
|
last.reserve(n);
|
|
for (std::size_t i = 0; i < n; ++i) {
|
|
auto ins = last.try_emplace(std::string_view(txs[i].txid), static_cast<int>(i));
|
|
if (ins.second) {
|
|
g.first[i] = static_cast<int>(i);
|
|
} else {
|
|
g.first[i] = g.first[static_cast<std::size_t>(ins.first->second)];
|
|
g.next[static_cast<std::size_t>(ins.first->second)] = static_cast<int>(i);
|
|
ins.first->second = static_cast<int>(i);
|
|
}
|
|
}
|
|
return g;
|
|
}
|
|
|
|
// Stable identity of a display row, used to keep an inline detail panel open across list rebuilds.
|
|
// txid alone is not unique (a tx can show several rows: multiple receive outputs, unmerged legs), so
|
|
// add the display type, address and the amount's exact bits. Two rows identical in all four are the same
|
|
// logical payment and expanding both is acceptable.
|
|
inline std::string rowKey(const std::string& txid, const std::string& displayType,
|
|
const std::string& address, double amount)
|
|
{
|
|
std::uint64_t bits = 0;
|
|
std::memcpy(&bits, &amount, sizeof bits);
|
|
char hex[17];
|
|
std::snprintf(hex, sizeof hex, "%016llx", static_cast<unsigned long long>(bits));
|
|
std::string k;
|
|
k.reserve(txid.size() + displayType.size() + address.size() + 20);
|
|
k.append(txid).push_back('|');
|
|
k.append(displayType).push_back('|');
|
|
k.append(address).push_back('|');
|
|
k.append(hex);
|
|
return k;
|
|
}
|
|
|
|
// The inputs that decide which rows the History list shows and in what order.
|
|
struct FilterInputs {
|
|
int type = 0;
|
|
int sort = 0;
|
|
std::string search;
|
|
};
|
|
|
|
// True (and `prev` updated) when type, sort or the FULL search text changed since last call. The caller
|
|
// then resets to page 1 and collapses the open row. Starts at the defaults (0, 0, "") so the first frame
|
|
// does not count as a change.
|
|
inline bool filterInputsChanged(FilterInputs& prev, int type, int sort, const std::string& search)
|
|
{
|
|
if (prev.type == type && prev.sort == sort && prev.search == search) return false;
|
|
prev.type = type;
|
|
prev.sort = sort;
|
|
prev.search = search;
|
|
return true;
|
|
}
|
|
|
|
} // namespace history
|
|
} // namespace ui
|
|
} // namespace dragonx
|