feat(ui): in-app FAQ, DPI-scaling audit fixes, mining/stratum polish + localize strings

Bundles the session's UI work (the touched files carry several of these
changes together, so they are committed as one coherent UI batch):

- FAQ: new RenderFaqDialog + data-driven faq_content, opened from a
  status-bar "?" (and the Windows title bar), styled like the Wallets
  modal with search, Wallet/Daemon tabs, and smooth scroll.
- DPI/font-scale audit: multiply hand-drawn absolute geometry by
  Layout::dpiScale() across ~30 files so nothing renders native-size at
  HiDPI / font_scale 1.5 (verified with a full sweep at 1.5x).
- Mining: chart now fills the horizontal space; thread stepper +/- buttons
  match the input-box height; move the stratum-host toggle into
  Node & Security (v1.3.0+).
- Settings: fix the auto-shield status text overlapping the grid.
- Sidebar: drop the peer-count badge on the Network button.
- i18n: wrap 193 hardcoded literals with TR() (keys/translations added in
  the preceding i18n commit), so the security/PIN/lock flow, seed-backup
  wizard, and witness-rebuild dialog localize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 19:37:16 -05:00
parent ed675f90d8
commit d27f387d6d
35 changed files with 867 additions and 366 deletions

View File

@@ -521,6 +521,8 @@ set(APP_SOURCES
src/ui/windows/settings_window.cpp
src/ui/pages/settings_page.cpp
src/ui/windows/about_dialog.cpp
src/ui/windows/faq_dialog.cpp
src/ui/windows/faq_content.cpp
src/ui/windows/key_export_dialog.cpp
src/ui/windows/transaction_details_dialog.cpp
src/ui/windows/qr_popup_dialog.cpp
@@ -657,6 +659,8 @@ set(APP_HEADERS
src/ui/windows/console_tab_helpers.h
src/ui/windows/settings_window.h
src/ui/windows/about_dialog.h
src/ui/windows/faq_dialog.h
src/ui/windows/faq_content.h
src/ui/windows/key_export_dialog.h
src/ui/windows/transaction_details_dialog.h
src/ui/windows/qr_popup_dialog.h

View File

@@ -965,6 +965,11 @@ edition-label = { position = 120 }
link-button = { width = 100, font = "button-sm" }
close-button = { width = 120, font = "button", align = "center" }
[dialogs.faq]
width = 860.0
height = 660.0
window = { width = 860, height = 660 }
[dialogs.settings]
width = 600.0
height = 550.0

View File

@@ -37,6 +37,7 @@
#include "ui/windows/market_tab.h"
#include "ui/windows/settings_window.h"
#include "ui/windows/about_dialog.h"
#include "ui/windows/faq_dialog.h"
#include "embedded/IconsMaterialDesign.h"
#include "ui/windows/key_export_dialog.h"
#include "ui/windows/transaction_details_dialog.h"
@@ -410,8 +411,8 @@ bool App::init()
settings_->setActiveWalletFile("wallet.dat");
settings_->save();
ui::Notifications::instance().warning(
"Your last-used wallet file (" + active + ") was not found — opened the default wallet "
"instead. If you moved it, restore it and switch back from the wallet list.", 20.0f);
std::string(TR("appx_last_used_wallet_not_found_prefix")) + active +
TR("appx_last_used_wallet_not_found_suffix"), 20.0f);
}
}
}
@@ -805,7 +806,7 @@ void App::update()
const std::string& err = lite_wallet_->lastOpenError();
if (!err.empty() && err != lite_open_error_) {
lite_open_error_ = err;
ui::Notifications::instance().error(std::string("Wallet open failed: ") + err, 8.0f);
ui::Notifications::instance().error(std::string(TR("appx_wallet_open_failed_prefix")) + err, 8.0f);
}
}
// Suppress the status bar's full-node connection-detail line in lite ("" and "Connected"
@@ -989,7 +990,7 @@ void App::update()
// poll (which hits the still-running pre-restart daemon, rescanning=false)
// would fire a false "complete" the instant rescan was clicked.
if (user_initiated_rescan_) {
ui::Notifications::instance().success("Blockchain rescan complete");
ui::Notifications::instance().success(TR("appx_blockchain_rescan_complete"));
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
}
resetWitnessRescanProgress();
@@ -1027,8 +1028,8 @@ void App::update()
state_.pool_mining.hashrate_15m = 0.0;
pool_starting_.store(false, std::memory_order_relaxed);
const std::string err = xmrig_manager_->getLastError();
ui::Notifications::instance().error(err.empty() ? "Miner stopped unexpectedly."
: ("Miner stopped: " + err));
ui::Notifications::instance().error(err.empty() ? TR("appx_miner_stopped_unexpectedly")
: (std::string(TR("appx_miner_stopped_prefix")) + err));
}
// Poll xmrig stats every ~2 seconds (use a simple toggle)
@@ -1053,7 +1054,7 @@ void App::update()
// Pool mining has a connect delay — announce once it's actually connected/hashing.
if (pool_starting_.load(std::memory_order_relaxed) && (ps.connected || ps.hashrate_10s > 0.0)) {
pool_starting_.store(false, std::memory_order_relaxed);
ui::Notifications::instance().success("Pool miner connected and hashing.");
ui::Notifications::instance().success(TR("appx_pool_miner_connected_and_hashing"));
}
// Get memory directly from OS (more reliable than API)
double memMB = xmrig_manager_->getMemoryUsageMB();
@@ -1095,7 +1096,7 @@ void App::update()
const std::string& status = scan.lastStatus;
if (scan.finished) {
if (state_.sync.rescanning && user_initiated_rescan_) {
ui::Notifications::instance().success("Blockchain rescan complete");
ui::Notifications::instance().success(TR("appx_blockchain_rescan_complete"));
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
}
// Witness rebuild finishes with the rescan it's part of.
@@ -1301,7 +1302,7 @@ void App::update()
!runtime_rescan_active_ && !bootstrap_downloading_ &&
state_.sync.blocks > 1) { // wait until the tip is known so the probe has a real range
post_bootstrap_rescan_pending_ = false;
ui::Notifications::instance().info("Bootstrap completereconciling your wallet with the new chain data.");
ui::Notifications::instance().info(TR("appx_bootstrap_complete_reconciling"));
detectLowestAvailableBlockHeight([this](bool ok, int lowest, bool fullHistory) {
if (ok && !fullHistory) {
runtimeRescan(lowest); // bootstrapped/pruned: rescan from the snapshot base
@@ -1323,7 +1324,16 @@ void App::update()
if (!state_.warming_up && !runtime_rescan_active_) {
if (network_refresh_.consumeDue(RefreshTimer::Transactions)) {
if (shouldRunWalletTransactionRefresh() && shouldRefreshTransactions()) {
refreshTransactionData();
// Throttle the routine new-block full history rescan (z_listreceivedbyaddress —
// O(mapWallet), holds cs_main) by its measured cost, so a large wallet doesn't
// re-scan every block and starve connection near the tip. Bypass when there's an
// explicit need: a dirty set, an in-progress multi-cycle shielded scan (which must
// continue to completion), or an in-flight send — all need fresh data immediately.
const bool txExplicitNeed = transactions_dirty_ || shielded_history_scan_pending_ ||
hasTransactionSendProgress() || !send_txids_.empty();
if (txExplicitNeed || txRefreshDue()) {
refreshTransactionData();
}
} else if (walletDataPage && shouldRefreshRecentTransactions()) {
refreshRecentTransactionData();
}
@@ -1339,7 +1349,13 @@ void App::update()
fastScanChatMemos();
}
if (network_refresh_.consumeDue(RefreshTimer::Addresses)) {
if (walletDataPage || addresses_dirty_ || hasTransactionSendProgress()) {
// Explicit need (a changed address set, or an in-flight send tracking its change output)
// must refresh now; the routine periodic poll on a wallet page is throttled by the last
// address scan's measured cost (addressRefreshDue) — this is the z_listunspent hammering
// that was starving cs_main while synced on a large wallet.
if (addresses_dirty_ || hasTransactionSendProgress()) {
refreshAddressData();
} else if (walletDataPage && addressRefreshDue()) {
refreshAddressData();
}
}
@@ -1396,7 +1412,7 @@ void App::handleGlobalShortcuts()
settings_->setSkinId(skins[cur].id);
settings_->save();
}
ui::Notifications::instance().info("Theme: " + skins[cur].name);
ui::Notifications::instance().info(std::string(TR("appx_theme_prefix")) + skins[cur].name);
}
}
}
@@ -1433,7 +1449,7 @@ void App::handleGlobalShortcuts()
ui::effects::ThemeEffects::instance().setReducedTransparency(!settings_->getThemeEffectsEnabled());
}
}
ui::Notifications::instance().info(newLow ? "Low-spec mode enabled" : "Low-spec mode disabled");
ui::Notifications::instance().info(newLow ? TR("appx_low_spec_mode_enabled") : TR("appx_low_spec_mode_disabled"));
}
// Keyboard shortcut: Ctrl+Down to toggle theme effects (Shift excluded)
@@ -1445,7 +1461,7 @@ void App::handleGlobalShortcuts()
settings_->setThemeEffectsEnabled(newState);
settings_->save();
}
ui::Notifications::instance().info(newState ? "Theme effects enabled" : "Theme effects disabled");
ui::Notifications::instance().info(newState ? TR("appx_theme_effects_enabled") : TR("appx_theme_effects_disabled"));
}
// Keyboard shortcut: Ctrl+Up to toggle simple gradient background
@@ -1454,7 +1470,7 @@ void App::handleGlobalShortcuts()
settings_->setGradientBackground(newGrad);
ui::schema::SkinManager::instance().setGradientMode(newGrad);
settings_->save();
ui::Notifications::instance().info(newGrad ? "Simple background enabled" : "Simple background disabled");
ui::Notifications::instance().info(newGrad ? TR("appx_simple_background_enabled") : TR("appx_simple_background_disabled"));
}
// Debug: Ctrl+Shift+W to re-show first-run wizard (set to false to disable)
@@ -1595,8 +1611,9 @@ void App::render()
{
int deletedN = pending_delete_result_.exchange(-1, std::memory_order_relaxed);
if (deletedN >= 0) {
ui::Notifications::instance().success("Blockchain data deleted (" + std::to_string(deletedN) +
" items). The daemon is restarting to re-sync from the network.");
char deletedMsg[160];
snprintf(deletedMsg, sizeof(deletedMsg), TR("appx_blockchain_data_deleted"), deletedN);
ui::Notifications::instance().success(deletedMsg);
}
}
@@ -2138,6 +2155,10 @@ void App::render()
ui::RenderAboutDialog(this, &show_about_);
}
if (show_faq_) {
ui::RenderFaqDialog(this, &show_faq_);
}
// Lite first-run welcome: prompt to create/restore when no wallet file exists yet.
renderLiteFirstRunPrompt();
// Lite send-time unlock prompt (shown when a spend is attempted on a locked wallet).
@@ -2881,6 +2902,31 @@ void App::renderStatusBar()
occupiedX = bellX;
}
// Help (?) — sits just left of the alert bell, on every platform. A plain, subtle question
// mark (no circle) that opens the FAQ.
{
ImFont* helpFont = ui::material::Type().iconSmall();
ImGui::PushFont(helpFont);
const float helpGlyphW = ImGui::CalcTextSize(ICON_MD_QUESTION_MARK).x;
ImGui::PopFont();
const float helpW = helpGlyphW + 10.0f * dp;
const float helpH = helpFont->LegacySize + 4.0f * dp;
const float helpX = occupiedX - helpW - gap;
ImGui::SameLine(helpX);
ui::material::IconButtonStyle hst;
hst.color = ui::material::OnSurfaceMedium();
hst.hoverColor = ui::material::OnSurface();
hst.hoverBg = ui::material::StateHover();
hst.bgRounding = 4.0f * dp;
hst.tooltip = TR("faq_open_tooltip");
if (ui::material::IconButton("##HelpFaq", ICON_MD_QUESTION_MARK, helpFont,
ImVec2(helpW, helpH), hst)) {
show_faq_ = true;
}
occupiedX = helpX;
}
// Version always at far right
ImGui::SameLine(versionX);
ImGui::Text("%s", versionBuf);
@@ -3005,7 +3051,8 @@ void App::renderLiteFirstRunPrompt()
if (ImGui::BeginPopupModal("##LiteFirstRun", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
const float btnW = 170.0f;
const float dp = ui::Layout::dpiScale();
const float btnW = 170.0f * dp;
if (step == 0) {
// ── Welcome ──────────────────────────────────────────────────────────
@@ -3013,7 +3060,7 @@ void App::renderLiteFirstRunPrompt()
ImGui::TextUnformatted(TR("lite_welcome_title"));
ImGui::PopFont();
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 360.0f);
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 360.0f * dp);
ImGui::TextUnformatted(TR("lite_welcome_msg"));
ImGui::PopTextWrapPos();
ImGui::Spacing(); ImGui::Spacing();
@@ -3021,7 +3068,7 @@ void App::renderLiteFirstRunPrompt()
if (creating) {
// Async create (with server failover) is in flight — driven to completion by
// App::update()'s pumpAsyncOpen(). Poll the controller for the outcome.
ImGui::TextUnformatted("Creating your wallet\xE2\x80\xA6");
ImGui::TextUnformatted(TR("appx_creating_your_wallet"));
if (lite_wallet_->walletOpen()) {
auto s = lite_wallet_->exportSeed(); // read the new seed back (local, fast)
if (s.ok && !s.seedPhrase.empty()) {
@@ -3039,7 +3086,7 @@ void App::renderLiteFirstRunPrompt()
} else if (!lite_wallet_->openInProgress() &&
!lite_wallet_->lastOpenError().empty()) {
ui::Notifications::instance().warning(
std::string("Create failed: ") + lite_wallet_->lastOpenError());
std::string(TR("appx_create_failed_prefix")) + lite_wallet_->lastOpenError());
creating = false; // back to the buttons so the user can retry
}
} else {
@@ -3065,14 +3112,12 @@ void App::renderLiteFirstRunPrompt()
} else if (step == 1) {
// ── Reveal the seed + birthday with backup warnings ─────────────────────
ImGui::PushFont(ui::material::Type().subtitle1());
ImGui::TextUnformatted("Back up your seed phrase");
ImGui::TextUnformatted(TR("appx_back_up_seed_phrase_title"));
ImGui::PopFont();
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f * dp);
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
ImGui::TextUnformatted("These 24 words are the ONLY way to restore your wallet. "
"Write them down in order, store them offline, and never share "
"them. If you lose them, your funds are gone forever.");
ImGui::TextUnformatted(TR("appx_seed_backup_warning"));
ImGui::PopStyleColor();
ImGui::PopTextWrapPos();
ImGui::Spacing();
@@ -3082,17 +3127,17 @@ void App::renderLiteFirstRunPrompt()
char cell[96];
snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str());
ImGui::TextUnformatted(cell);
if ((i % 4) != 3 && i + 1 < words.size()) ImGui::SameLine(((i % 4) + 1) * 130.0f);
if ((i % 4) != 3 && i + 1 < words.size()) ImGui::SameLine(((i % 4) + 1) * 130.0f * dp);
}
ImGui::Spacing();
char bday[80];
snprintf(bday, sizeof(bday), "Birthday (block height): %llu — back this up too.", birthday);
snprintf(bday, sizeof(bday), TR("appx_birthday_block_height"), birthday);
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
ImGui::TextUnformatted(bday);
ImGui::PopStyleColor();
ImGui::Spacing(); ImGui::Spacing();
if (ui::material::TactileButton("I've written it down", ImVec2(btnW, 0))) {
if (ui::material::TactileButton(TR("appx_ive_written_it_down"), ImVec2(btnW, 0))) {
chips.clear();
for (const auto& w : words) chips.emplace_back(w, false);
std::mt19937 rng{std::random_device{}()};
@@ -3102,9 +3147,9 @@ void App::renderLiteFirstRunPrompt()
step = 2;
}
ImGui::SameLine();
if (ui::material::TactileButton("Copy", ImVec2(80, 0))) copySecretToClipboard(seed);
if (ui::material::TactileButton(TR("appx_copy"), ImVec2(80 * dp, 0))) copySecretToClipboard(seed);
ImGui::SameLine();
if (ui::material::TactileButton(skipConfirm ? "Skip anyway" : "Skip", ImVec2(120, 0))) {
if (ui::material::TactileButton(skipConfirm ? TR("appx_skip_anyway") : TR("appx_skip"), ImVec2(120, 0))) {
if (!skipConfirm) {
skipConfirm = true; // require a second, deliberate click
} else {
@@ -3116,27 +3161,26 @@ void App::renderLiteFirstRunPrompt()
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
ImGui::TextUnformatted("You have not backed up your seed — funds could be lost. "
"Skip anyway?");
ImGui::TextUnformatted(TR("appx_seed_not_backed_up_warning"));
ImGui::PopStyleColor();
ImGui::PopTextWrapPos();
}
} else if (step == 2) {
// ── Verify: tap the words in order ──────────────────────────────────────
ImGui::PushFont(ui::material::Type().subtitle1());
ImGui::TextUnformatted("Confirm your backup");
ImGui::TextUnformatted(TR("appx_confirm_your_backup"));
ImGui::PopFont();
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::TextUnformatted("Tap the words in the correct order to confirm you saved them.");
ImGui::TextUnformatted(TR("appx_tap_words_in_order"));
ImGui::PopTextWrapPos();
ImGui::Spacing();
ImGui::TextDisabled("Progress: %d / %d", progress, (int)words.size());
ImGui::TextDisabled(TR("appx_progress_n_of_n"), progress, (int)words.size());
if (ImGui::GetTime() < wrongFlashUntil) {
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
ImGui::TextUnformatted(" — that's not the next word");
ImGui::TextUnformatted(TR("appx_not_next_word"));
ImGui::PopStyleColor();
}
ImGui::Spacing();
@@ -3145,9 +3189,9 @@ void App::renderLiteFirstRunPrompt()
ImGui::PushID((int)i);
if (chips[i].second) {
ImGui::BeginDisabled();
ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125, 0));
ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125 * dp, 0));
ImGui::EndDisabled();
} else if (ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125, 0))) {
} else if (ui::material::TactileButton(chips[i].first.c_str(), ImVec2(125 * dp, 0))) {
if (progress < (int)words.size() && chips[i].first == words[progress]) {
chips[i].second = true; // correct next word
++progress;
@@ -3162,15 +3206,15 @@ void App::renderLiteFirstRunPrompt()
const bool verified = progress == (int)words.size();
if (!verified) ImGui::BeginDisabled();
if (ui::material::TactileButton("Done", ImVec2(btnW, 0))) {
ui::Notifications::instance().success("Wallet created and backed up.", 6.0f);
if (ui::material::TactileButton(TR("appx_done"), ImVec2(btnW, 0))) {
ui::Notifications::instance().success(TR("appx_wallet_created_and_backed_up"), 6.0f);
finish();
}
if (!verified) ImGui::EndDisabled();
ImGui::SameLine();
if (ui::material::TactileButton("Back", ImVec2(80, 0))) { skipConfirm = false; step = 1; }
if (ui::material::TactileButton(TR("appx_back"), ImVec2(80, 0))) { skipConfirm = false; step = 1; }
ImGui::SameLine();
if (ui::material::TactileButton(skipConfirm ? "Skip anyway" : "Skip", ImVec2(120, 0))) {
if (ui::material::TactileButton(skipConfirm ? TR("appx_skip_anyway") : TR("appx_skip"), ImVec2(120, 0))) {
if (!skipConfirm) {
skipConfirm = true; // require a second, deliberate click
} else {
@@ -3182,8 +3226,7 @@ void App::renderLiteFirstRunPrompt()
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
ImGui::TextUnformatted("You have not backed up your seed — funds could be lost. "
"Skip anyway?");
ImGui::TextUnformatted(TR("appx_seed_not_backed_up_warning"));
ImGui::PopStyleColor();
ImGui::PopTextWrapPos();
}
@@ -3200,7 +3243,7 @@ void App::renderLiteFirstRunPrompt()
if (restoring) {
// Async restore (with server failover) in flight — driven by pumpAsyncOpen().
ImGui::TextUnformatted("Restoring your wallet\xE2\x80\xA6");
ImGui::TextUnformatted(TR("appx_restoring_your_wallet"));
if (lite_wallet_->walletOpen()) {
ui::Notifications::instance().success(TR("lite_restore_ok"), 6.0f);
finish(); // wipes restoreSeed; the wallet then syncs from the lite server
@@ -3212,10 +3255,10 @@ void App::renderLiteFirstRunPrompt()
} else {
ImGui::TextUnformatted(TR("lite_restore_seed_label"));
ImGui::InputTextMultiline("##LiteRestoreSeed", restoreSeed, sizeof(restoreSeed),
ImVec2(380.0f, ImGui::GetTextLineHeight() * 3.2f));
ImVec2(380.0f * dp, ImGui::GetTextLineHeight() * 3.2f));
ImGui::Spacing();
ImGui::TextUnformatted(TR("lite_restore_birthday_label"));
ImGui::SetNextItemWidth(160.0f);
ImGui::SetNextItemWidth(160.0f * dp);
ImGui::InputInt("##LiteRestoreBirthday", &restoreBirthday);
if (restoreBirthday < 0) restoreBirthday = 0;
@@ -3242,8 +3285,10 @@ void App::renderLiteFirstRunPrompt()
if (!seedTrim.empty() && !seedLenOk) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::Warning()));
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::TextUnformatted(("Recovery phrase should be 24 words — you have " +
std::to_string(seedWords) + ".").c_str());
char recoveryWordsMsg[96];
snprintf(recoveryWordsMsg, sizeof(recoveryWordsMsg),
TR("appx_recovery_phrase_word_count"), seedWords);
ImGui::TextUnformatted(recoveryWordsMsg);
ImGui::PopTextWrapPos();
ImGui::PopStyleColor();
ImGui::Spacing();
@@ -3260,13 +3305,13 @@ void App::renderLiteFirstRunPrompt()
restoring = true;
} else {
restoreErr = lite_wallet_->lastOpenError().empty()
? std::string("Could not start restore")
? std::string(TR("appx_could_not_start_restore"))
: lite_wallet_->lastOpenError();
}
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ui::material::TactileButton("Back", ImVec2(80, 0))) {
if (ui::material::TactileButton(TR("appx_back"), ImVec2(80, 0))) {
sodium_memzero(restoreSeed, sizeof(restoreSeed));
restoreErr.clear();
step = 0;
@@ -3290,17 +3335,18 @@ void App::renderLiteUnlockPrompt()
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("##LiteUnlock", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
const float dp = ui::Layout::dpiScale();
ImGui::PushFont(ui::material::Type().subtitle1());
ImGui::TextUnformatted(TR("lite_unlock_title"));
ImGui::PopFont();
ImGui::Spacing();
ImGui::TextUnformatted(TR("lite_unlock_msg"));
ImGui::Spacing();
ImGui::SetNextItemWidth(280.0f);
ImGui::SetNextItemWidth(280.0f * dp);
const bool entered = ImGui::InputText("##LiteUnlockPassModal", pass, sizeof(pass),
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::Spacing();
const float btnW = 130.0f;
const float btnW = 130.0f * dp;
bool doUnlock = ui::material::TactileButton(TR("lite_unlock_btn"), ImVec2(btnW, 0)) || entered;
if (doUnlock) {
const bool ok = lite_wallet_->unlockWallet(pass);
@@ -5072,7 +5118,7 @@ void App::handlePaymentURI(const std::string& uri)
auto payment = util::parsePaymentURI(uri);
if (!payment.valid) {
ui::Notifications::instance().error("Invalid payment URI: " + payment.error);
ui::Notifications::instance().error(std::string(TR("appx_invalid_payment_uri_prefix")) + payment.error);
return;
}
@@ -5087,7 +5133,7 @@ void App::handlePaymentURI(const std::string& uri)
setCurrentPage(ui::NavPage::Send);
// Notify user
std::string msg = "Payment request loaded";
std::string msg = TR("appx_payment_request_loaded");
if (payment.amount > 0) {
char buf[64];
snprintf(buf, sizeof(buf), " for %.8f DRGX", payment.amount);
@@ -5436,7 +5482,7 @@ bool App::stopDaemonForWalletSwitch()
void App::rescanBlockchain()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
@@ -5451,17 +5497,17 @@ void App::rescanBlockchain()
// Re-entrancy guard: a rescan/repair (both drive state_.sync.rescanning) or this exact task already
// running would stomp each other — a second confirm must not launch a duplicate operation.
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
return;
}
// Don't race a wallet switch / seed-adopt / encryption restart, which drive their own daemon stop/start.
if (daemon_restarting_) {
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
ui::Notifications::instance().warning(TR("appx_node_busy_restarting"));
return;
}
DEBUG_LOGF("[App] Starting blockchain rescan - stopping daemon first\n");
ui::Notifications::instance().info("Restarting daemon with -rescan flag...");
ui::Notifications::instance().info(TR("appx_restarting_daemon_rescan_flag"));
// Initialize rescan state for status bar display. rescan_confirmed_active_ stays false until we
// actually observe the restarted daemon rescanning — so the first poll (which may still reach the
@@ -5491,7 +5537,7 @@ void App::rescanBlockchain()
void App::repairWallet()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
@@ -5504,16 +5550,16 @@ void App::repairWallet()
}
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
return;
}
if (daemon_restarting_) {
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
ui::Notifications::instance().warning(TR("appx_node_busy_restarting"));
return;
}
DEBUG_LOGF("[App] Starting wallet repair (-zapwallettxes=2) - stopping daemon first\n");
ui::Notifications::instance().info("Restarting daemon with -zapwallettxes=2 (wallet repair)...");
ui::Notifications::instance().info(TR("appx_restarting_daemon_zapwallettxes"));
// -zapwallettxes=2 deletes and rebuilds every wallet tx/note record, then rescans the whole
// chain — so reuse the rescan status UI (status bar + warmup-end completion detection). Same
@@ -5542,11 +5588,11 @@ void App::repairWallet()
void App::reinstallBundledDaemon()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
if (!resources::getBundledDaemonInfo().available) {
ui::Notifications::instance().warning("This build has no bundled daemon to install");
ui::Notifications::instance().warning(TR("appx_no_bundled_daemon_to_install"));
return;
}
// Require embedded-daemon *support*, but NOT an active daemon_controller_. When the wallet is
@@ -5554,17 +5600,17 @@ void App::reinstallBundledDaemon()
// we can still RPC-stop that node, overwrite the binaries, and bring up our own managed daemon —
// which is exactly the state that previously blocked "Install bundled" with a cryptic warning.
if (!supportsEmbeddedDaemon()) {
ui::Notifications::instance().warning("This build has no embedded daemon to install");
ui::Notifications::instance().warning(TR("appx_no_embedded_daemon_to_install"));
return;
}
if (async_tasks_.isRunning("reinstall-daemon")) {
ui::Notifications::instance().warning("The daemon reinstall is already in progress.");
ui::Notifications::instance().warning(TR("appx_daemon_reinstall_in_progress"));
return;
}
DEBUG_LOGF("[App] Reinstalling bundled daemon binary — stopping daemon first\n");
ui::Notifications::instance().info("Installing bundled daemon — the node will stop, update, and restart...");
ui::Notifications::instance().info(TR("appx_installing_bundled_daemon"));
async_tasks_.submit("reinstall-daemon", [this](const util::AsyncTaskManager::Token& token) {
AppDaemonLifecycleRuntime runtime(*this);
@@ -5596,7 +5642,7 @@ void App::reinstallBundledDaemon()
void App::deleteBlockchainData()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
@@ -5609,12 +5655,12 @@ void App::deleteBlockchainData()
}
if (state_.sync.rescanning || async_tasks_.isRunning(decision.taskName)) {
ui::Notifications::instance().warning("A blockchain maintenance operation is already in progress.");
ui::Notifications::instance().warning(TR("appx_blockchain_maintenance_in_progress"));
return;
}
DEBUG_LOGF("[App] Deleting blockchain data - stopping daemon first\n");
ui::Notifications::instance().info("Stopping daemon and deleting blockchain data...");
ui::Notifications::instance().info(TR("appx_stopping_daemon_deleting_blockchain"));
daemon_controller_->prepareLifecycleOperation(decision, settings_.get());
async_tasks_.submit(decision.taskName, [this, decision](const util::AsyncTaskManager::Token& token) {
@@ -5719,7 +5765,7 @@ void App::beginShutdown()
// Stop xmrig pool miner before stopping the daemon
if (xmrig_manager_ && xmrig_manager_->isRunning()) {
shutdown_status_ = "Stopping pool miner...";
shutdown_status_ = TR("appx_stopping_pool_miner");
xmrig_manager_->stop(3000);
}
@@ -5731,7 +5777,7 @@ void App::beginShutdown()
// Worker join + RPC disconnect happen in shutdown().
if (!daemon_controller_) {
DEBUG_LOGF("beginShutdown: no embedded daemon, disconnecting only\n");
shutdown_status_ = "Disconnecting...";
shutdown_status_ = TR("appx_disconnecting");
if (settings_) {
settings_->save();
}
@@ -5762,15 +5808,15 @@ void App::beginShutdown()
// modal "Please wait" dialog).
shutdown_thread_ = std::thread([this]() {
DEBUG_LOGF("shutdown thread: calling stopEmbeddedDaemon()\n");
shutdown_status_ = "Sending stop command to daemon...";
shutdown_status_ = TR("appx_sending_stop_command_to_daemon");
// Send RPC stop command
stopEmbeddedDaemon();
DEBUG_LOGF("shutdown thread: daemon stopped, disconnecting RPC\n");
shutdown_status_ = "Cleaning up...";
shutdown_status_ = TR("appx_cleaning_up");
DEBUG_LOGF("shutdown thread: complete\n");
shutdown_status_ = "Shutdown complete";
shutdown_status_ = TR("appx_shutdown_complete");
shutdown_complete_ = true;
});
}
@@ -5883,18 +5929,16 @@ void App::renderDaemonStopConfirm()
if (ImGui::BeginPopupModal("##DaemonStopConfirm", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
if (Type().subtitle1()) ImGui::PushFont(Type().subtitle1());
ImGui::TextUnformatted("Node is rebuilding its witness cache");
ImGui::TextUnformatted(TR("appx_node_rebuilding_witness_cache"));
if (Type().subtitle1()) ImGui::PopFont();
ImGui::Spacing();
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 26.0f);
ImGui::TextUnformatted(
"Stopping the node now discards the in-progress rebuild and restarts it (several minutes) "
"the next time you open the wallet. You can keep the node running instead.");
ImGui::TextUnformatted(TR("appx_stopping_node_discards_rebuild"));
ImGui::PopTextWrapPos();
ImGui::Spacing();
ImGui::Spacing();
if (TactileButton("Keep node running & quit", ImVec2(0, 0))) {
if (TactileButton(TR("appx_keep_node_running_and_quit"), ImVec2(0, 0))) {
shutdown_keep_daemon_override_ = true;
shutdown_confirmed_ = true;
daemon_stop_confirm_open_ = false;
@@ -5905,7 +5949,7 @@ void App::renderDaemonStopConfirm()
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 210)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Error()));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Error(), 160)));
const bool stopAnyway = TactileButton("Stop anyway & quit", ImVec2(0, 0));
const bool stopAnyway = TactileButton(TR("appx_stop_anyway_and_quit"), ImVec2(0, 0));
ImGui::PopStyleColor(3);
if (stopAnyway) {
shutdown_confirmed_ = true;
@@ -5914,7 +5958,7 @@ void App::renderDaemonStopConfirm()
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (TactileButton("Cancel", ImVec2(0, 0))) {
if (TactileButton(TR("appx_cancel"), ImVec2(0, 0))) {
daemon_stop_confirm_open_ = false; // abort the quit; stay open
ImGui::CloseCurrentPopup();
}
@@ -5930,6 +5974,7 @@ void App::renderDaemonStopConfirm()
void App::renderShutdownScreen()
{
using namespace ui::material;
const float dp = ui::Layout::dpiScale();
auto shutElem = [](const char* key, float fb) {
float v = ui::schema::UI().drawElement("components.shutdown", key).size;
return v >= 0 ? v : fb;
@@ -5983,7 +6028,7 @@ void App::renderShutdownScreen()
// -------------------------------------------------------------------
float lineH = ImGui::GetTextLineHeightWithSpacing();
float titleH = Type().h5() ? Type().h5()->LegacySize : lineH * 1.5f;
float spinnerD = shutElem("spinner-radius", 20.0f) * 2.0f + 12.0f;
float spinnerD = shutElem("spinner-radius", 20.0f) * dp * 2.0f + 12.0f * dp;
float statusH = lineH * 2.0f;
float sepH = lineH;
float panelH = shutElem("panel-max-height", 160.0f);
@@ -6014,10 +6059,10 @@ void App::renderShutdownScreen()
// 2. Animated arc spinner
// -------------------------------------------------------------------
{
float r = shutElem("spinner-radius", 20.0f);
float thick = shutElem("spinner-thickness", 3.0f);
float r = shutElem("spinner-radius", 20.0f) * dp;
float thick = shutElem("spinner-thickness", 3.0f) * dp;
// Screen-space centre for draw list
ImVec2 sc(wp.x + cx, wp.y + ImGui::GetCursorPosY() + r + 2.0f);
ImVec2 sc(wp.x + cx, wp.y + ImGui::GetCursorPosY() + r + 2.0f * dp);
// Background ring (dim)
dl->PathArcTo(sc, r, 0.0f, kPi * 2.0f, 48);
@@ -6031,7 +6076,7 @@ void App::renderShutdownScreen()
dl->PathStroke(ui::schema::UI().resolveColor("var(--spinner-active)", IM_COL32(255, 218, 0, 200)), 0, thick);
// Advance cursor past the spinner
ImGui::Dummy(ImVec2(0, r * 2.0f + 8.0f));
ImGui::Dummy(ImVec2(0, r * 2.0f + 8.0f * dp));
}
ImGui::Spacing();
@@ -6056,9 +6101,9 @@ void App::renderShutdownScreen()
char elapsed[64];
int secs = (int)shutdown_timer_;
if (secs < 60)
snprintf(elapsed, sizeof(elapsed), "%d seconds", secs);
snprintf(elapsed, sizeof(elapsed), TR("appx_n_seconds"), secs);
else
snprintf(elapsed, sizeof(elapsed), "%d min %d sec", secs / 60, secs % 60);
snprintf(elapsed, sizeof(elapsed), TR("appx_n_min_n_sec"), secs / 60, secs % 60);
ImGui::PushFont(Type().caption());
ImVec2 ts = ImGui::CalcTextSize(elapsed);
@@ -6078,7 +6123,8 @@ void App::renderShutdownScreen()
// State-aware caution: while the status is a daemon flush/exit step, force-quitting risks the
// chainstate; say so instead of a bare button.
if (shutdownStalled && !curShut.empty()) {
std::string stalledMsg = "Still \"" + curShut + "\" — force quitting now may corrupt chain data.";
std::string stalledMsg = std::string(TR("appx_still_status_prefix")) + curShut +
TR("appx_still_status_suffix");
ImVec2 ms = ImGui::CalcTextSize(stalledMsg.c_str());
ImGui::SetCursorPosX(cx - ms.x * 0.5f);
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
@@ -6087,7 +6133,7 @@ void App::renderShutdownScreen()
ImGui::Spacing();
}
const char* forceLabel = TR("force_quit");
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f, 0);
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f * dp, 0);
ImGui::SetCursorPosX(cx - btnSize.x * 0.5f);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
@@ -6117,7 +6163,7 @@ void App::renderShutdownScreen()
ImGui::Spacing();
ImGui::Spacing();
float btnW = 120.0f;
float btnW = 120.0f * dp;
float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x;
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) * 0.5f);
@@ -6149,7 +6195,7 @@ void App::renderShutdownScreen()
ImVec2 p0(wp.x + pad, wp.y + ImGui::GetCursorPosY());
ImVec2 p1(wp.x + vp_size.x - pad, p0.y);
dl->AddLine(p0, p1, ui::schema::UI().resolveColor("var(--status-divider)", IM_COL32(255, 255, 255, 30)), 1.0f);
ImGui::Dummy(ImVec2(0, 4.0f));
ImGui::Dummy(ImVec2(0, 4.0f * dp));
}
ImGui::Spacing();
@@ -6182,7 +6228,7 @@ void App::renderShutdownScreen()
ImGui::GetCursorPosY() + panelPad));
ImGui::PushFont(Type().caption());
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.45f, 0.45f, 0.50f, 1.0f));
ImGui::TextUnformatted("dragonxd output");
ImGui::TextUnformatted(TR("appx_dragonxd_output"));
ImGui::PopStyleColor();
ImGui::PopFont();
@@ -6222,7 +6268,7 @@ void App::renderShutdownScreen()
// Advance cursor past the panel
float panelBottom = panelMax.y - wp.y;
ImGui::SetCursorPosY(panelBottom + 4.0f);
ImGui::SetCursorPosY(panelBottom + 4.0f * dp);
ImGui::Dummy(ImVec2(0, 0));
}
}
@@ -6417,14 +6463,14 @@ void App::renderLoadingOverlay(float contentH)
char wbuf[128];
if (state_.sync.witness_phase == 2 && progress > 0.01f && state_.sync.witness_remaining > 0)
snprintf(wbuf, sizeof(wbuf), "Rebuilding witness cache %.0f%% — %d blocks left",
snprintf(wbuf, sizeof(wbuf), TR("appx_rebuilding_witness_cache_blocks_left"),
progress * 100.0f, state_.sync.witness_remaining);
else if (state_.sync.witness_phase == 2 && progress > 0.01f)
snprintf(wbuf, sizeof(wbuf), "Rebuilding witness cache %.0f%%", progress * 100.0f);
snprintf(wbuf, sizeof(wbuf), TR("appx_rebuilding_witness_cache_pct"), progress * 100.0f);
else if (progress > 0.01f)
snprintf(wbuf, sizeof(wbuf), "Setting initial Sapling witnesses %.0f%%", progress * 100.0f);
snprintf(wbuf, sizeof(wbuf), TR("appx_setting_initial_sapling_witnesses"), progress * 100.0f);
else
snprintf(wbuf, sizeof(wbuf), "Rebuilding Sapling note witnesses");
snprintf(wbuf, sizeof(wbuf), "%s", TR("appx_rebuilding_sapling_note_witnesses"));
ImFont* capFont = Type().caption();
if (!capFont) capFont = ImGui::GetFont();
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, wbuf);
@@ -6462,7 +6508,7 @@ void App::renderLoadingOverlay(float contentH)
// Progress text — "Syncing 45.2% — Block 123456 / 234567"
char syncBuf[128];
snprintf(syncBuf, sizeof(syncBuf), "Syncing %.1f%% — Block %d / %d",
snprintf(syncBuf, sizeof(syncBuf), TR("appx_syncing_pct_block_n_of_n"),
progress * 100.0f, state_.sync.blocks, state_.sync.headers);
ImFont* capFont = Type().caption();
if (!capFont) capFont = ImGui::GetFont();
@@ -6474,7 +6520,7 @@ void App::renderLoadingOverlay(float contentH)
} else if (!state_.connected && state_.sync.blocks > 0) {
// Show last known block height while reconnecting
char blockBuf[64];
snprintf(blockBuf, sizeof(blockBuf), "Last block: %d", state_.sync.blocks);
snprintf(blockBuf, sizeof(blockBuf), TR("appx_last_block_n"), state_.sync.blocks);
ImFont* capFont = Type().caption();
if (!capFont) capFont = ImGui::GetFont();
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, blockBuf);
@@ -6493,8 +6539,8 @@ void App::renderLoadingOverlay(float contentH)
if (!capFont) capFont = ImGui::GetFont();
const char* encLabel = encrypt_in_progress_
? "Encrypting wallet..."
: "Waiting for daemon to encrypt wallet...";
? TR("appx_encrypting_wallet")
: TR("appx_waiting_for_daemon_to_encrypt_wallet");
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, encLabel);
ImU32 encCol = IM_COL32(255, 218, 0, 200);
dl->AddText(capFont, capFont->LegacySize,
@@ -6529,7 +6575,7 @@ void App::renderLoadingOverlay(float contentH)
if (!bodyFont2) bodyFont2 = ImGui::GetFont();
// Error title
const char* errTitle = "Daemon Error";
const char* errTitle = TR("appx_daemon_error");
ImVec2 ts = bodyFont2->CalcTextSizeA(bodyFont2->LegacySize, FLT_MAX, 0.0f, errTitle);
dl->AddText(bodyFont2, bodyFont2->LegacySize,
ImVec2(wp.x + cx - ts.x * 0.5f, curY),
@@ -6549,7 +6595,7 @@ void App::renderLoadingOverlay(float contentH)
}
// Crash count hint
if (daemon_controller_->crashCount() >= 3) {
const char* hint = "Use Settings > Restart Daemon to try again";
const char* hint = TR("appx_use_settings_restart_daemon_hint");
ImVec2 hs2 = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, hint);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(wp.x + cx - hs2.x * 0.5f, curY),
@@ -6822,7 +6868,7 @@ void App::copySecretToClipboard(const std::string& secret)
clipboard_secret_hash_ = secret.empty() ? 0 : h;
clipboard_clear_deadline_ = secret.empty() ? 0.0 : (ImGui::GetTime() + 45.0);
if (!secret.empty())
ui::Notifications::instance().info("Copiedclipboard auto-clears in 45s", 4.0f);
ui::Notifications::instance().info(TR("appx_copied_clipboard_autoclears"), 4.0f);
}
void App::clearSecretClipboardIfArmed()
@@ -6916,7 +6962,7 @@ std::string App::buildDiagnosticsReport()
void App::restartDaemon()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
@@ -6956,7 +7002,7 @@ void App::restartDaemon()
void App::reindexBlockDatabase()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
return;
}
if (!daemon_controller_) return;

View File

@@ -462,6 +462,7 @@ public:
return 0;
}
void showAboutDialog() { show_about_ = true; }
void showFaqDialog() { show_faq_ = true; }
// Legacy tab compat — maps int to NavPage
void setCurrentTab(int tab);
@@ -800,6 +801,7 @@ private:
// balance poll discards per-note data). Mirrors chat_fast_scan_in_flight_.
bool chat_note_scan_in_flight_ = false;
double chat_note_scan_last_ = 0.0; // ImGui time of the last note scan (rate limit)
double chat_note_scan_ms_ = 0.0; // measured cost of the last note scan (adaptive back-off)
// Most-recent chain tip, cached across refreshes: a lite refresh model that carries spendableOutputs
// may NOT carry sync status that same cycle (tolerated partial refresh), so verifiedSelfNoteCount reads
// this cache rather than requiring the current model to have both — else the budget flickers to 0.
@@ -809,6 +811,9 @@ private:
// Coordinator helpers (both variants unless noted; see app_network.cpp).
void refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model); // lite: recompute caches on a fresh model
void refreshChatNoteBudgetNode(); // full node: rate-limited z_listunspent worker scan
// Feeds the SAME chat send-budget from an already-collected z_listunspent (the address refresh),
// so the dedicated scan above is skipped while the address refresh is active (dedup — see #3).
void updateChatNoteBudgetFromUnspent(const std::vector<services::NetworkRefreshService::UnspentNoteLite>& unspentNotes);
void pumpChatNoteBuffer(); // per-frame: drain the queue / build the buffer
int verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
int pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
@@ -877,6 +882,8 @@ private:
void fastScanChatMemos();
bool chat_fast_scan_in_flight_ = false; // guard against overlapping fast-scan RPCs
float chat_fast_scan_accum_ = 0.0f; // seconds since the last fast-scan (dedicated ~2.5s poll)
double chat_fast_scan_last_ = 0.0; // ImGui time of the last 0-conf fast scan (adaptive back-off)
double chat_fast_scan_ms_ = 0.0; // measured cost of the last fast scan (z_listreceivedbyaddress)
bool font_rebuild_requested_ = false; // set by requestFontRebuild(); consumed in preFrame()
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
bool lite_firstrun_dismissed_ = false;
@@ -1028,6 +1035,7 @@ private:
bool show_demo_window_ = false;
bool show_settings_ = false;
bool show_about_ = false;
bool show_faq_ = false;
bool show_import_key_ = false;
bool show_export_key_ = false;
bool show_backup_ = false;
@@ -1143,6 +1151,14 @@ private:
std::time_t sync_settle_until_ = 0; // hold the sync-throttle until this wall-clock time (0 = not settling)
double last_balance_scan_ms_ = 0.0; // measured cost of the last z_gettotalbalance scan
bool force_balance_refresh_ = false; // a wallet mutation forces the next balance poll through the throttle
// Same adaptive back-off applied to the other two O(mapWallet) scans that hold the daemon's cs_main:
// the address scan (z_listunspent) and the history scan (z_listreceivedbyaddress). Without this, a
// large shielded wallet re-scans them every tab cadence (~seconds each), saturating cs_main and
// starving block connection near the tip (where effectivelySyncing() reads false). See
// addressRefreshDue() / txRefreshDue(); only the routine periodic poll is throttled — explicit
// refreshes (tab switch, dirty set, in-flight send) call the refresh directly and bypass this.
double last_address_scan_ms_ = 0.0; // measured cost of the last address scan
double last_tx_scan_ms_ = 0.0; // measured cost of the last history scan
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
std::uint64_t clipboard_secret_hash_ = 0;
double clipboard_clear_deadline_ = 0.0;
@@ -1498,6 +1514,11 @@ private:
void applyRefreshPolicy(ui::NavPage page);
bool effectivelySyncing() const; // syncing, or within the post-sync settle window (hysteresis)
bool balanceRefreshDue() const; // adaptive: enough time elapsed given the last balance-scan cost?
bool addressRefreshDue() const; // same adaptive back-off for the address scan (z_listunspent)
bool txRefreshDue() const; // same adaptive back-off for the history scan (z_listreceivedbyaddress)
// Shared duty-cycle rule: a scan may resume only once (lastScanMs / kScanDutyCycle) has elapsed since
// lastUpdate, so any single O(mapWallet) scan can occupy at most ~kScanDutyCycle of wall-clock.
bool scanRefreshDue(std::int64_t lastUpdate, double lastScanMs) const;
bool currentPageNeedsWalletDataRefresh() const;
bool shouldRunWalletTransactionRefresh() const;
bool shouldRefreshTransactions() const;

View File

@@ -300,14 +300,14 @@ void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestar
});
} else {
ui::Notifications::instance().warning(
"Please restart your daemon for encryption to take effect.");
TR("sec_restart_daemon_for_encryption"));
}
}
void App::encryptWalletWithPassphrase(const std::string& passphrase) {
if (!rpc_ || !rpc_->isConnected()) return;
encrypt_in_progress_ = true;
encrypt_status_ = "Encrypting wallet...";
encrypt_status_ = TR("sec_encrypting_wallet");
if (worker_) {
worker_->post([this, passphrase]() mutable -> rpc::RPCWorker::MainCb {
@@ -317,7 +317,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
if (result.encrypted) {
return [this]() {
encrypt_in_progress_ = false;
encrypt_status_ = "Wallet encrypted. Restarting daemon...";
encrypt_status_ = TR("sec_wallet_encrypted_restarting_daemon");
DEBUG_LOGF("[App] Wallet encrypted — restarting daemon\n");
// Immediately update local encryption state so the
@@ -336,7 +336,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
}
ui::Notifications::instance().info(
"Wallet encrypted successfully", 5.0f);
TR("sec_wallet_encrypted_successfully"), 5.0f);
// The daemon shuts itself down after encryptwallet.
// Update connection_status_ so the loading overlay
@@ -348,11 +348,11 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
std::string err = result.error;
return [this, err]() {
encrypt_in_progress_ = false;
encrypt_status_ = "Encryption failed: " + err;
encrypt_status_ = std::string(TR("sec_encryption_failed_prefix")) + err;
DEBUG_LOGF("[App] encryptwallet failed: %s\n", err.c_str());
ui::Notifications::instance().error(
"Encryption failed: " + err);
std::string(TR("sec_encryption_failed_prefix")) + err);
// Return to passphrase entry on failure
if (show_encrypt_dialog_ &&
@@ -393,7 +393,7 @@ void App::processDeferredEncryption() {
std::string pin = std::move(deferredEncryption.pin);
encrypt_in_progress_ = true;
encrypt_status_ = "Encrypting wallet...";
encrypt_status_ = TR("sec_encrypting_wallet");
if (worker_) {
worker_->post([this, request = services::WalletSecurityController::DeferredEncryptionSnapshot{std::move(passphrase), std::move(pin)}]() mutable -> rpc::RPCWorker::MainCb {
@@ -413,13 +413,13 @@ void App::processDeferredEncryption() {
if (result.pinStored) {
settings_->setPinEnabled(true);
settings_->save();
ui::Notifications::instance().info("Wallet encrypted & PIN set", 5.0f);
ui::Notifications::instance().info(TR("sec_wallet_encrypted_and_pin_set"), 5.0f);
} else {
ui::Notifications::instance().warning(
"Wallet encrypted but PIN vault failed");
TR("sec_wallet_encrypted_but_pin_vault_failed"));
}
} else {
ui::Notifications::instance().info("Wallet encrypted successfully", 5.0f);
ui::Notifications::instance().info(TR("sec_wallet_encrypted_successfully"), 5.0f);
}
wallet_security_.clearDeferredEncryption();
@@ -432,9 +432,9 @@ void App::processDeferredEncryption() {
std::string err = result.error;
return [this, err]() {
encrypt_in_progress_ = false;
encrypt_status_ = "Encryption failed: " + err;
encrypt_status_ = std::string(TR("sec_encryption_failed_prefix")) + err;
DEBUG_LOGF("[App] Deferred encryptwallet failed: %s\n", err.c_str());
ui::Notifications::instance().error("Encryption failed: " + err);
ui::Notifications::instance().error(std::string(TR("sec_encryption_failed_prefix")) + err);
wallet_security_.clearDeferredEncryption();
};
}
@@ -531,7 +531,7 @@ void App::lockWallet() {
if (!lock_failure_warned_) {
lock_failure_warned_ = true;
ui::Notifications::instance().warning(
"Couldn't lock the wallet — it is still unlocked. Check the daemon connection.", 12.0f);
TR("sec_couldnt_lock_wallet"), 12.0f);
}
}
};
@@ -541,7 +541,7 @@ void App::lockWallet() {
void App::changePassphrase(const std::string& oldPass, const std::string& newPass) {
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
encrypt_in_progress_ = true;
encrypt_status_ = "Changing passphrase...";
encrypt_status_ = TR("sec_changing_passphrase");
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
@@ -574,9 +574,9 @@ void App::changePassphrase(const std::string& oldPass, const std::string& newPas
memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_));
unlockTransactionHistoryCacheWithPassphrase(newPass);
storeTransactionHistoryCacheIfAvailable();
ui::Notifications::instance().info("Passphrase changed successfully");
ui::Notifications::instance().info(TR("sec_passphrase_changed_successfully"));
} else {
encrypt_status_ = "Failed: " + err_msg;
encrypt_status_ = std::string(TR("sec_failed_prefix")) + err_msg;
}
util::SecureVault::secureZero(newPass.data(), newPass.size());
};
@@ -637,8 +637,7 @@ void App::refreshWalletEncryptionState() {
!encryption_incomplete_warned_) {
encryption_incomplete_warned_ = true;
ui::Notifications::instance().warning(
"Wallet encryption did not complete — your wallet is NOT encrypted. "
"Open Settings to finish encrypting it.", 30.0f);
TR("sec_encryption_did_not_complete"), 30.0f);
}
if (state_.transactions.empty()) {
loadTransactionHistoryCacheIfAvailable();
@@ -951,7 +950,7 @@ void App::renderLockScreen() {
ImU32 textCol = ui::material::OnSurface();
{
const char* title = "Wallet Locked";
const char* title = TR("sec_wallet_locked_title");
ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title);
dl->AddText(titleFont, titleFont->LegacySize,
ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title);
@@ -964,7 +963,7 @@ void App::renderLockScreen() {
if (lock_lockout_timer_ < 0) lock_lockout_timer_ = 0;
char msg[128];
snprintf(msg, sizeof(msg), "Too many attempts. Wait %.0f seconds...", lock_lockout_timer_);
snprintf(msg, sizeof(msg), TR("sec_too_many_attempts_wait"), lock_lockout_timer_);
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg);
@@ -977,10 +976,10 @@ void App::renderLockScreen() {
// Mode toggle (PIN / Passphrase) — only show if PIN vault exists
if (hasPinVault) {
const char* modeIcon = lock_use_pin_ ? ICON_MD_DIALPAD : ICON_MD_PASSWORD;
const char* modeText = lock_use_pin_ ? " PIN" : " Passphrase";
const char* modeText = lock_use_pin_ ? " PIN" : TR("sec_mode_passphrase");
const char* switchLabel = lock_use_pin_
? "Use passphrase instead"
: "Use PIN instead";
? TR("sec_use_passphrase_instead")
: TR("sec_use_pin_instead");
// Current mode indicator — icon with icon font, text with caption font
ImFont* iconFont = ui::material::Type().iconSmall();
@@ -1082,7 +1081,7 @@ void App::renderLockScreen() {
if (lock_unlock_in_progress_) {
// Animated spinner dots
char msg[64];
snprintf(msg, sizeof(msg), "Unlocking%s", ui::material::LoadingDots());
snprintf(msg, sizeof(msg), TR("sec_unlocking_fmt"), ui::material::LoadingDots());
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy),
@@ -1106,7 +1105,7 @@ void App::renderLockScreen() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!canSubmit);
bool btnClicked = ui::material::TactileButton("Unlock", ImVec2(unlockW, unlockH));
bool btnClicked = ui::material::TactileButton(TR("sec_unlock_button"), ImVec2(unlockW, unlockH));
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
@@ -1160,7 +1159,7 @@ void App::renderLockScreen() {
r->call("walletpassphrase", {passphrase, timeout});
rpcOk = true;
} else {
rpcErr = "Not connected to daemon";
rpcErr = TR("sec_not_connected_to_daemon");
}
} catch (const std::exception& e) {
rpcErr = e.what();
@@ -1176,7 +1175,7 @@ void App::renderLockScreen() {
// so route through applyUnlockFailure so the lockout curve applies
// (this path previously bumped the counter but skipped the lockout math).
return [this, rpcErr, passphrase = std::move(passphrase)]() mutable {
applyUnlockFailure("Unlock failed: " + rpcErr);
applyUnlockFailure(std::string(TR("sec_unlock_failed_prefix")) + rpcErr);
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
};
}
@@ -1579,7 +1578,7 @@ void App::renderDecryptWalletDialog() {
if (!passphrase.empty()) sodium_memzero(&passphrase[0], passphrase.size());
if (!unlock.ok) {
return [this]() {
wallet_security_workflow_.failEntry("Incorrect passphrase");
wallet_security_workflow_.failEntry(TR("sec_incorrect_passphrase_decrypt"));
};
}
@@ -1695,7 +1694,7 @@ void App::renderDecryptWalletDialog() {
});
ui::Notifications::instance().info(
"Importing keys & rescanning blockchain — wallet is usable while this runs",
TR("sec_importing_keys_rescanning"),
8.0f);
};
});
@@ -1714,7 +1713,7 @@ void App::renderDecryptWalletDialog() {
wallet_security_workflow_.finishImport();
ui::Notifications::instance().error(
err +
"\nEncrypted backup: wallet.dat.encrypted.bak",
TR("sec_encrypted_backup_suffix"),
12.0f);
};
});
@@ -1740,7 +1739,7 @@ void App::renderDecryptWalletDialog() {
refreshPeerInfo();
ui::Notifications::instance().success(
"Wallet decrypted successfully! All keys imported.",
TR("sec_wallet_decrypted_all_keys_imported"),
8.0f);
DEBUG_LOGF("[App] Wallet decrypted successfully\n");
};
@@ -1862,7 +1861,7 @@ void App::renderDecryptWalletDialog() {
int tMins = (int)(totalElapsed / 60);
int tSecs = (int)(totalElapsed % 60);
ImGui::Spacing();
ImGui::TextDisabled("Total elapsed: %dm %02ds", tMins, tSecs);
ImGui::TextDisabled(TR("sec_total_elapsed_fmt"), tMins, tSecs);
}
// ---- Phase 2: Success ----
@@ -1965,7 +1964,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
pin_in_progress_ = true;
pin_status_ = "Verifying passphrase...";
pin_status_ = TR("sec_verifying_passphrase");
// Verify passphrase + store vault on worker thread to avoid
// blocking the UI with Argon2id key derivation.
@@ -1985,7 +1984,7 @@ void App::renderPinDialogs() {
if (!passphrase.empty()) util::SecureVault::secureZero(&passphrase[0], passphrase.size());
if (!pin.empty()) util::SecureVault::secureZero(&pin[0], pin.size());
return [this]() {
pin_status_ = "Incorrect passphrase";
pin_status_ = TR("sec_incorrect_passphrase_pin_setup");
pin_in_progress_ = false;
};
}
@@ -2009,15 +2008,15 @@ void App::renderPinDialogs() {
pin_status_.clear();
pin_in_progress_ = false;
show_pin_setup_ = false;
ui::Notifications::instance().info("PIN set successfully");
ui::Notifications::instance().info(TR("sec_pin_set_successfully"));
} else {
pin_status_ = "Failed to create vault";
pin_status_ = TR("sec_failed_to_create_vault");
pin_in_progress_ = false;
}
};
});
} else {
pin_status_ = "Not connected to daemon";
pin_status_ = TR("sec_not_connected_to_daemon_pin");
pin_in_progress_ = false;
}
}
@@ -2082,7 +2081,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) {
pin_in_progress_ = true;
pin_status_ = "Changing PIN...";
pin_status_ = TR("sec_changing_pin");
std::string oldPin(pin_old_buf_);
std::string newPinCopy = newPin;
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
@@ -2098,15 +2097,15 @@ void App::renderPinDialogs() {
pin_status_.clear();
pin_in_progress_ = false;
show_pin_change_ = false;
ui::Notifications::instance().info("PIN changed successfully");
ui::Notifications::instance().info(TR("sec_pin_changed_successfully"));
} else {
pin_status_ = "Incorrect current PIN";
pin_status_ = TR("sec_incorrect_current_pin");
pin_in_progress_ = false;
}
};
});
} else {
pin_status_ = "Internal error";
pin_status_ = TR("sec_internal_error_change_pin");
pin_in_progress_ = false;
}
}
@@ -2147,7 +2146,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) {
pin_in_progress_ = true;
pin_status_ = "Verifying PIN...";
pin_status_ = TR("sec_verifying_pin");
std::string oldPin(pin_old_buf_);
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
@@ -2167,15 +2166,15 @@ void App::renderPinDialogs() {
pin_status_.clear();
pin_in_progress_ = false;
show_pin_remove_ = false;
ui::Notifications::instance().info("PIN removed");
ui::Notifications::instance().info(TR("sec_pin_removed"));
} else {
pin_status_ = "Incorrect PIN";
pin_status_ = TR("sec_incorrect_pin_remove");
pin_in_progress_ = false;
}
};
});
} else {
pin_status_ = "Internal error";
pin_status_ = TR("sec_internal_error_remove_pin");
pin_in_progress_ = false;
}
}

View File

@@ -310,6 +310,8 @@ void App::buildSweepCatalog()
[](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::hide(); });
add("modal-backup", ui::NavPage::Overview,
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
add("modal-faq", ui::NavPage::Overview,
[](App& a) { a.show_faq_ = true; }, [](App& a) { a.show_faq_ = false; });
// Encrypt-wallet dialog — the redesigned passphrase-entry phase (never fires the async encrypt).
add("modal-encrypt", ui::NavPage::Settings,
[](App& a) { a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry; a.show_encrypt_dialog_ = true; },
@@ -708,9 +710,22 @@ void App::startSweepImpl(bool full)
if (sweep_current_theme_only_)
sweep_skins_.assign(1, ui::schema::SkinManager::instance().activeSkinId());
// DEV/TEST hook (dormant unless the env is set): DRAGONX_SWEEP_ONLY="send,receive" restricts the
// sweep to the named surfaces and the dark skin, so a slow large-window run captures just the tab
// under review instead of all surfaces x every skin.
const char* sweepOnly = std::getenv("DRAGONX_SWEEP_ONLY");
std::string sweepOnlyStr = sweepOnly ? sweepOnly : "";
if (!sweepOnlyStr.empty()) sweep_skins_.assign(1, std::string("dark"));
sweep_full_ = full;
if (full) { capture_mode_ = true; installDemoWalletData(); }
buildSweepCatalog();
if (!sweepOnlyStr.empty()) {
std::vector<SweepTarget> keep;
for (const auto& t : sweep_targets_)
if (sweepOnlyStr.find(t.name) != std::string::npos) keep.push_back(t);
sweep_targets_.swap(keep);
}
if (sweep_targets_.empty()) { if (full) { clearDemoWalletData(); capture_mode_ = false; sweep_full_ = false; } return; }
sweep_dir_ = full ? screenshotFullDir() : screenshotDir();

View File

@@ -5,6 +5,7 @@
#include "theme_effects.h"
#include "low_spec.h"
#include "../schema/ui_schema.h"
#include "../layout.h"
#include <cmath>
#include <cstdlib>
#include <cstring>
@@ -58,6 +59,7 @@ void ThemeEffects::beginFrame() {
void ThemeEffects::loadFromTheme() {
auto& S = schema::UI();
const float dp = Layout::dpiScale();
auto eff = [&](const char* name) {
return S.drawElement("effects", name);
};
@@ -98,7 +100,7 @@ void ThemeEffects::loadFromTheme() {
// ---- Shimmer ----
shimmer_.enabled = eff("shimmer-enabled").sizeOr(0.0f) > 0.5f;
shimmer_.speed = eff("shimmer-speed").sizeOr(0.12f);
shimmer_.width = eff("shimmer-width").sizeOr(80.0f);
shimmer_.width = eff("shimmer-width").sizeOr(80.0f) * dp;
shimmer_.alpha = eff("shimmer-alpha").sizeOr(0.06f);
shimmer_.angle = eff("shimmer-angle").sizeOr(30.0f);
// Shimmer color: read from the schema's color resolver
@@ -124,7 +126,7 @@ void ThemeEffects::loadFromTheme() {
glow_pulse_.speed = eff("glow-pulse-speed").sizeOr(2.0f);
glow_pulse_.minAlpha = eff("glow-pulse-min-alpha").sizeOr(0.0f);
glow_pulse_.maxAlpha = eff("glow-pulse-max-alpha").sizeOr(0.15f);
glow_pulse_.radius = eff("glow-pulse-radius").sizeOr(4.0f);
glow_pulse_.radius = eff("glow-pulse-radius").sizeOr(4.0f) * dp;
auto glowColorElem = eff("glow-pulse-color");
if (!glowColorElem.color.empty()) {
glow_pulse_.color = S.resolveColor(glowColorElem.color, IM_COL32(255, 218, 0, 255));
@@ -136,7 +138,7 @@ void ThemeEffects::loadFromTheme() {
edge_trace_.enabled = eff("edge-trace-enabled").sizeOr(0.0f) > 0.5f;
edge_trace_.speed = eff("edge-trace-speed").sizeOr(0.3f);
edge_trace_.length = eff("edge-trace-length").sizeOr(0.20f);
edge_trace_.thickness = eff("edge-trace-thickness").sizeOr(1.5f);
edge_trace_.thickness = eff("edge-trace-thickness").sizeOr(1.5f) * dp;
edge_trace_.alpha = eff("edge-trace-alpha").sizeOr(0.6f);
auto edgeColorElem = eff("edge-trace-color");
if (!edgeColorElem.color.empty()) {
@@ -149,7 +151,7 @@ void ThemeEffects::loadFromTheme() {
ember_rise_.enabled = eff("ember-rise-enabled").sizeOr(0.0f) > 0.5f;
ember_rise_.count = (int)eff("ember-rise-count").sizeOr(8.0f);
ember_rise_.speed = eff("ember-rise-speed").sizeOr(0.4f);
ember_rise_.particleSize = eff("ember-rise-particle-size").sizeOr(1.5f);
ember_rise_.particleSize = eff("ember-rise-particle-size").sizeOr(1.5f) * dp;
ember_rise_.alpha = eff("ember-rise-alpha").sizeOr(0.5f);
auto emberColorElem = eff("ember-rise-color");
if (!emberColorElem.color.empty()) {
@@ -165,7 +167,7 @@ void ThemeEffects::loadFromTheme() {
// accent (e.g. Obsidian) are unaffected; Jade turns it on as its hero.
gradient_border_.panels = eff("gradient-border-panels").sizeOr(0.0f) > 0.5f;
gradient_border_.speed = eff("gradient-border-speed").sizeOr(0.15f);
gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f);
gradient_border_.thickness = eff("gradient-border-thickness").sizeOr(1.5f) * dp;
gradient_border_.alpha = eff("gradient-border-alpha").sizeOr(0.6f);
auto gbColorA = eff("gradient-border-color-a");
if (!gbColorA.color.empty()) {
@@ -194,7 +196,7 @@ void ThemeEffects::loadFromTheme() {
sandstorm_.count = (int)eff("sandstorm-count").sizeOr(80.0f);
sandstorm_.speed = eff("sandstorm-speed").sizeOr(0.35f);
sandstorm_.windAngle = eff("sandstorm-wind-angle").sizeOr(15.0f);
sandstorm_.particleSize = eff("sandstorm-particle-size").sizeOr(1.5f);
sandstorm_.particleSize = eff("sandstorm-particle-size").sizeOr(1.5f) * dp;
sandstorm_.alpha = eff("sandstorm-alpha").sizeOr(0.35f);
sandstorm_.gustSpeed = eff("sandstorm-gust-speed").sizeOr(0.07f);
sandstorm_.gustStrength = eff("sandstorm-gust-strength").sizeOr(0.4f);
@@ -694,6 +696,7 @@ void ThemeEffects::drawEdgeTrace(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax,
void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const {
if (!enabled_ || !ember_rise_.enabled) return;
const float dp = Layout::dpiScale();
float w = pMax.x - pMin.x;
float h = pMax.y - pMin.y;
if (w <= 0 || h <= 0) return;
@@ -707,7 +710,7 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
// Deterministic pseudo-random x position per particle
// Simple hash: sin of large prime multiples
float xHash = std::sin((float)(i + 1) * 127.1f) * 0.5f + 0.5f;
float xDrift = std::sin(time_ * 0.7f + i * 2.4f) * 4.0f; // gentle sway
float xDrift = std::sin(time_ * 0.7f + i * 2.4f) * 4.0f * dp; // gentle sway
float x = pMin.x + w * xHash + xDrift;
float y = pMax.y - phase * (h + 8.0f); // rise from bottom past top
@@ -745,6 +748,7 @@ void ThemeEffects::drawEmberRise(ImDrawList* dl, ImVec2 pMin, ImVec2 pMax) const
void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
if (!enabled_ || !ember_rise_.enabled) return;
const float dp = Layout::dpiScale();
ImGuiViewport* vp = ImGui::GetMainViewport();
float vpW = vp->WorkSize.x;
float vpH = vp->WorkSize.y;
@@ -765,7 +769,7 @@ void ThemeEffects::drawViewportEmbers(ImDrawList* dl) const {
float xHash = std::sin((float)(i + 1) * 127.1f) * 43758.5453f;
xHash = xHash - (int)xHash; // fractional part
if (xHash < 0) xHash += 1.0f;
float xDrift = std::sin(time_ * 0.5f + i * 1.7f) * 8.0f;
float xDrift = std::sin(time_ * 0.5f + i * 1.7f) * 8.0f * dp;
float x = vpX + vpW * xHash + xDrift;
float y = vpY + vpH * (1.0f - phase); // rise from bottom to top

View File

@@ -925,7 +925,7 @@ inline void DrawStatCard(ImDrawList* dl,
// Draw a full-height rounded rect with card rounding (left corners)
// and clip to stripe width so the shape follows the corner radius.
if ((card.accentCol & IM_COL32_A_MASK) != 0) {
float stripeW = 4.0f;
float stripeW = 4.0f * Layout::dpiScale();
dl->PushClipRect(cMin, ImVec2(cMin.x + stripeW, cMax.y), true);
dl->AddRectFilled(cMin, cMax, card.accentCol, rnd,
ImDrawFlags_RoundCornersLeft);
@@ -1282,7 +1282,8 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 winPos = ImGui::GetWindowPos();
float winWidth = ImGui::GetWindowWidth();
float barHeight = 36.0f;
const float dp = Layout::dpiScale();
float barHeight = 36.0f * dp;
// Get accent color from theme if not provided
if (!accent_col) {
@@ -1306,15 +1307,15 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
ImFont* titleFont = Type().subtitle1();
ImGui::PushFont(titleFont);
ImVec2 titleSize = ImGui::CalcTextSize(title);
float titleX = barMin.x + 16.0f;
float titleX = barMin.x + 16.0f * dp;
float titleY = barMin.y + (barHeight - titleSize.y) * 0.5f;
DrawTextShadow(dl, ImVec2(titleX, titleY), OnSurface(), title);
ImGui::PopFont();
// Close button (X) on right side
if (p_open) {
float btnSize = 24.0f;
float btnX = barMax.x - btnSize - 12.0f;
float btnSize = 24.0f * dp;
float btnX = barMax.x - btnSize - 12.0f * dp;
float btnY = barMin.y + (barHeight - btnSize) * 0.5f;
ImVec2 btnMin(btnX, btnY);
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
@@ -1327,7 +1328,7 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
// Button background on hover
if (hovered) {
dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, held ? 40 : 25), 4.0f);
dl->AddRectFilled(btnMin, btnMax, IM_COL32(255, 255, 255, held ? 40 : 25), 4.0f * dp);
}
// Draw X icon
@@ -1348,7 +1349,7 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
}
// Reserve space for title bar so content starts below it
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + barHeight + 8.0f * dp);
return closeClicked;
}
@@ -1648,7 +1649,7 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
// Fill/border alpha govern every overlay dialog's card boundary — kept well above the default
// glass panel so the card reads as a distinct surface over busy backdrops (tx lists, mining
// tiles, chat) while staying translucent rather than opaque.
cardGlass.rounding = 16.0f; cardGlass.fillAlpha = 60; cardGlass.borderAlpha = 90; cardGlass.borderWidth = 1.0f;
cardGlass.rounding = 16.0f * dp; cardGlass.fillAlpha = 60; cardGlass.borderAlpha = 90; cardGlass.borderWidth = 1.0f;
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
}
@@ -1662,8 +1663,8 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
// Content child.
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f : 16.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28, 20) : ImVec2(28, 24));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f * dp : 16.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28 * dp, 20 * dp) : ImVec2(28 * dp, 24 * dp));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // transparent (glass/blur behind)
// A card with a known height is a fixed frame (fixed-height dialogs, and auto-height dialogs whose
// content overflowed the viewport); otherwise the child auto-resizes to its content.

View File

@@ -580,11 +580,11 @@ enum SettingsTab { TAB_APPEARANCE = 0, TAB_WALLET, TAB_BACKUP, TAB_NODE, TAB_EXP
static void renderSettingsTabBar(float availWidth) {
using namespace material;
struct T { int id; const char* label; const char* idstr; };
static const T tabs[] = {
{TAB_APPEARANCE, "Appearance", "##stabA"}, {TAB_WALLET, "Wallet", "##stabW"},
{TAB_BACKUP, "Backup & Data", "##stabB"}, {TAB_NODE, "Node & Security", "##stabN"},
{TAB_EXPLORER, "Explorer", "##stabE"}, {TAB_CHAT, "Chat", "##stabC"},
{TAB_ABOUT, "About", "##stabT"},
const T tabs[] = {
{TAB_APPEARANCE, TR("grpa_tab_appearance"), "##stabA"}, {TAB_WALLET, TR("grpa_tab_wallet"), "##stabW"},
{TAB_BACKUP, TR("grpa_tab_backup_data"), "##stabB"}, {TAB_NODE, TR("grpa_tab_node_security"), "##stabN"},
{TAB_EXPLORER, TR("grpa_tab_explorer"), "##stabE"}, {TAB_CHAT, TR("grpa_tab_chat"), "##stabC"},
{TAB_ABOUT, TR("grpa_tab_about"), "##stabT"},
};
ImDrawList* dl = ImGui::GetWindowDrawList();
ImFont* f = Type().body2();
@@ -811,7 +811,7 @@ void RenderSettingsPage(App* app) {
if (!skin.valid) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::BeginDisabled(true);
std::string lbl = skin.name + " (invalid)";
std::string lbl = skin.name + TR("grpa_invalid_suffix");
ImGui::Selectable(lbl.c_str(), false);
ImGui::EndDisabled();
ImGui::PopStyleColor();
@@ -1196,14 +1196,25 @@ void RenderSettingsPage(App* app) {
// O1: when a v1.3.0+ node auto-shields coinbase itself, show its real state — the checkbox
// above only governs the wallet's own fallback shielder (which defers to the node). Nothing
// renders on pre-1.3.0 daemons (never probed), so their behaviour is unchanged.
if (app && app->daemonAutoShieldProbed()) {
// This status must NOT flow with ImGui's cursor: the checkbox grid is absolutely positioned
// (SetCursorScreenPos at rowY), so a normal-flow Text would land under the auto-shield checkbox
// and the next grid row (Use Tor / Keep daemon) would draw on top of it. Instead, draw it on its
// own full-width row at the grid's current rowY, wrapped to the card, then advance rowY past it.
if (app && app->daemonAutoShieldProbed() &&
(app->daemonAutoShieldActive() || !app->daemonAutoShieldDisabledReason().empty())) {
ImGui::SetCursorScreenPos(ImVec2(cx, rowY));
ImGui::PushTextWrapPos((cx + cw) - ImGui::GetWindowPos().x); // wrap to the card content width
if (app->daemonAutoShieldActive()) {
ImGui::TextColored(ImVec4(0.40f, 0.78f, 0.40f, 1.0f), " %s", TR("autoshield_by_node"));
if (!app->daemonAutoShieldAddress().empty())
ImGui::TextDisabled(" %s", app->daemonAutoShieldAddress().c_str());
} else if (!app->daemonAutoShieldDisabledReason().empty()) {
} else {
ImGui::TextDisabled(" %s", app->daemonAutoShieldDisabledReason().c_str());
}
ImGui::PopTextWrapPos();
last = ImGui::GetCursorScreenPos().y; // cursor is now below the status text
rowY = last + gp; // next grid row starts below it
c = 0;
}
CB(TrId("use_tor", "tor"), &s_settingsState.use_tor);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tor"));
@@ -1214,26 +1225,7 @@ void RenderSettingsPage(App* app) {
if (CB(TrId("stop_external", "stop_ext"), &s_settingsState.stop_external_daemon))
saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stop_external"));
// O2: host a RandomX stratum pool from this node. Only offered on daemons that implement
// it (v1.3.0+, version encoded major*1e6+minor*1e4+rev*100+build), so we never show a
// toggle that does nothing. Takes effect on the next daemon start/restart. Blank allow-IP
// = loopback only (safe); a subnet opens it to that LAN.
if (app->state().daemon_version >= 1030000) {
if (CB(TrId("stratum_host", "strat_host"), &s_settingsState.stratum_host))
saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stratum_host"));
if (s_settingsState.stratum_host) {
ImGui::TextDisabled(" %s", TR("stratum_host_hint"));
ImGui::SetNextItemWidth(220.0f * Layout::dpiScale());
if (ImGui::InputTextWithHint("##stratumallowip", TR("stratum_allowip_hint"),
s_settingsState.stratum_allowip, sizeof(s_settingsState.stratum_allowip)))
saveSettingsPageState(app->settings());
if (s_settingsState.stratum_allowip[0] != '\0')
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.25f, 1.0f), " %s",
TR("stratum_expose_warn"));
}
}
// (Stratum pool hosting lives in the Node & Security tab — it's a node feature.)
}
if (CB(TrId("verbose_logging", "verbose"), &s_settingsState.verbose_logging)) {
dragonx::util::Logger::instance().setVerbose(s_settingsState.verbose_logging);
@@ -1697,7 +1689,7 @@ void RenderSettingsPage(App* app) {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("lite_birthday_label"));
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
ImGui::SetNextItemWidth(std::min(160.0f * dp, liteInputW));
ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_birthday"));
if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0;
@@ -1709,7 +1701,7 @@ void RenderSettingsPage(App* app) {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("lite_account_label"));
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
ImGui::SetNextItemWidth(std::min(160.0f * dp, liteInputW));
ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_account"));
if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0;
@@ -1889,7 +1881,7 @@ void RenderSettingsPage(App* app) {
while (!liteKey.empty() && (liteKey.front()==' '||liteKey.front()=='\t'||liteKey.front()=='\n'||liteKey.front()=='\r')) liteKey.erase(liteKey.begin());
while (!liteKey.empty() && (liteKey.back()==' '||liteKey.back()=='\t'||liteKey.back()=='\n'||liteKey.back()=='\r')) liteKey.pop_back();
if (liteKey.empty()) {
s_settingsState.lite_backup_status = "Enter a private key to import.";
s_settingsState.lite_backup_status = TR("grpa_enter_private_key_to_import");
} else {
const auto r = app->liteWallet()->importKey(liteKey);
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key));
@@ -2650,6 +2642,10 @@ void RenderSettingsPage(App* app) {
if (material::ActionButton("##aboutbug", TR("report_bug"), ICON_MD_BUG_REPORT, AT::Secondary))
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues");
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_report_bug"));
lf.next(material::ActionButtonWidth(TR("faq"), ICON_MD_QUESTION_MARK));
if (material::ActionButton("##aboutfaq", TR("faq"), ICON_MD_QUESTION_MARK, AT::Secondary))
app->showFaqDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("faq_open_tooltip"));
}
const float rightBottom = ImGui::GetCursorScreenPos().y;
@@ -2676,6 +2672,33 @@ void RenderSettingsPage(App* app) {
if (s_settingsState.current_tab == TAB_NODE)
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// MINING POOL HOSTING — run this node's built-in RandomX stratum server so other miners can point
// at this machine. It's a node feature, so it belongs here (not the Wallet tab). Full-node only, and
// only on a daemon new enough to implement -stratum (v1.3.0+, version encoded
// major*1e6+minor*1e4+rev*100+build). Takes effect on the next daemon start/restart; a blank allow-IP
// keeps it loopback-only (safe), a subnet opens it to that LAN.
// ====================================================================
if (app->supportsFullNodeLifecycleActions() && s_settingsState.current_tab == TAB_NODE &&
app->state().daemon_version >= 1030000) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("stratum_host_section"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
material::GlassCardScope card(dl, availWidth, pad, bottomPad, glassSpec);
if (ImGui::Checkbox(TR("stratum_host"), &s_settingsState.stratum_host))
saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stratum_host"));
if (s_settingsState.stratum_host) {
ImGui::TextDisabled(" %s", TR("stratum_host_hint"));
ImGui::SetNextItemWidth(220.0f * Layout::dpiScale());
if (ImGui::InputTextWithHint("##stratumallowip", TR("stratum_allowip_hint"),
s_settingsState.stratum_allowip, sizeof(s_settingsState.stratum_allowip)))
saveSettingsPageState(app->settings());
if (s_settingsState.stratum_allowip[0] != '\0')
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.25f, 1.0f), " %s",
TR("stratum_expose_warn"));
}
}
// ====================================================================
// DEBUG OPTIONS — collapsible card (full-node only: holds the screenshot sweep + the dragonxd
// daemon debug= categories written to DRAGONX.conf; lite has no daemon). Shown on the Node tab.
@@ -2738,7 +2761,7 @@ void RenderSettingsPage(App* app) {
if (chat::hushChatFeatureEnabledAtBuild()) {
// Populate the Chat tab with demo conversations so the sweep captures its real UI.
ImGui::SameLine();
if (TactileButton("Seed demo chat", ImVec2(0, 0), S.resolveFont("button")))
if (TactileButton(TR("grpa_seed_demo_chat"), ImVec2(0, 0), S.resolveFont("button")))
app->seedChatDemoData();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_demo_chat"));
}
@@ -2765,29 +2788,29 @@ void RenderSettingsPage(App* app) {
"paymentdisclosure", "pow", "proxy", "prune", "rand",
"reindex", "rpc", "selectcoins", "tor", "zmq", "zrpc"
};
static const char* debugTips[] = {
"Peer address tracking and management",
"Alert system messages",
"Benchmark timings for operations",
"Coin database read/write operations",
"Berkeley DB operations",
"Fee estimation algorithm",
"HTTP RPC server activity",
"Libevent networking library",
"Lock contention debugging",
"Transaction memory pool activity",
"Network connections and messages",
"Payment disclosure protocol",
"Proof-of-work mining activity",
"SOCKS5 proxy connections",
"Block pruning operations",
"Random number generation",
"Blockchain reindexing progress",
"RPC command processing",
"Coin selection for transactions",
"Tor integration and circuit info",
"ZeroMQ notification system",
"Shielded (z-addr) RPC operations"
const char* debugTips[] = {
TR("grpa_dbg_addrman"),
TR("grpa_dbg_alert"),
TR("grpa_dbg_bench"),
TR("grpa_dbg_coindb"),
TR("grpa_dbg_db"),
TR("grpa_dbg_estimatefee"),
TR("grpa_dbg_http"),
TR("grpa_dbg_libevent"),
TR("grpa_dbg_lock"),
TR("grpa_dbg_mempool"),
TR("grpa_dbg_net"),
TR("grpa_dbg_paymentdisclosure"),
TR("grpa_dbg_pow"),
TR("grpa_dbg_proxy"),
TR("grpa_dbg_prune"),
TR("grpa_dbg_rand"),
TR("grpa_dbg_reindex"),
TR("grpa_dbg_rpc"),
TR("grpa_dbg_selectcoins"),
TR("grpa_dbg_tor"),
TR("grpa_dbg_zmq"),
TR("grpa_dbg_zrpc")
};
constexpr int numCats = sizeof(debugCats) / sizeof(debugCats[0]);
@@ -3037,7 +3060,7 @@ void RenderSettingsPage(App* app) {
ImGui::TextWrapped("%s", TR("rescan_bootstrapped_msg"));
ImGui::Spacing();
ImGui::Text("%s", TR("rescan_from_height"));
ImGui::SetNextItemWidth(160.0f);
ImGui::SetNextItemWidth(160.0f * dp);
ImGui::InputInt("##rescanHeight", &s_settingsState.rescan_start_height);
if (s_settingsState.rescan_start_height < 0) s_settingsState.rescan_start_height = 0;
} else {
@@ -3051,12 +3074,12 @@ void RenderSettingsPage(App* app) {
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (material::TactileButton(TrId("cancel", "rescan_cancel").c_str(), ImVec2(btnW, 40))) {
if (material::TactileButton(TrId("cancel", "rescan_cancel").c_str(), ImVec2(btnW, 40 * dp))) {
s_settingsState.confirm_rescan = false;
}
ImGui::SameLine();
ImGui::BeginDisabled(detecting);
if (material::TactileButton(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40))) {
if (material::TactileButton(TrId("rescan", "rescan_confirm").c_str(), ImVec2(btnW, 40 * dp))) {
if (bootstrapped) {
app->runtimeRescan(s_settingsState.rescan_start_height);
} else {

View File

@@ -219,7 +219,7 @@ inline void DrawGlassCutout(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
float lineW = s_cc.lineW;
// --- Outer glow pass: wider, softer dark edge on top-left ---
float glowExpand = s_cc.glowExpand;
float glowExpand = s_cc.glowExpand * Layout::dpiScale();
int glowA = (int)s_cc.glowAlpha;
float glowLineW = s_cc.glowLineW;
{
@@ -289,6 +289,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
// inner pass gives crisp bevel edge. All use AddRect with rounding so
// every layer follows the rounded corners perfectly — no clip rects needed.
{
const float dp = Layout::dpiScale();
float cx = (mn.x + mx.x) * 0.5f;
float cy = (mn.y + mx.y) * 0.5f;
@@ -304,7 +305,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
struct BevelPass { float expand; float lineW; float fadeStart; float fadeEnd; };
BevelPass passes[] = {
{ 0.5f, 0.75f, 0.30f, 0.55f }, // Outer glow (thin)
{ 0.5f * dp, 0.75f, 0.30f, 0.55f }, // Outer glow (thin)
{ 0.0f, 0.75f, 0.38f, 0.58f }, // Inner crisp bevel
};
@@ -387,7 +388,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
}
}
if (depth > s_ic.threshold) {
float baseInset = s_ic.inset;
float baseInset = s_ic.inset * Layout::dpiScale();
int shadowMax = (int)(s_ic.maxAlpha * depth);
float fadeRatio = s_ic.fadeRatio;
float bW = mx.x - mn.x - baseInset * 2.0f;
@@ -487,7 +488,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
float fixedH = stripH; // collapse strip
for (int i = 0; i < (int)NavPage::Count_; ++i)
if (IsNavPageVisible(kNavItems[i].page) && kNavItems[i].section_label && showLabels)
fixedH += olFsz + 2.0f + sectionLabelPadBot; // section label + pad below
fixedH += olFsz + 2.0f * dp + sectionLabelPadBot; // section label + pad below
fixedH += bottomPadding + stripH; // exit area
float baseFlexH = baseNavGap;
@@ -525,7 +526,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
if (showLabels) {
curY += sectionGap;
if (nSectionLabels < 4) sectionLabelY[nSectionLabels++] = curY;
curY += olFsz + 2.0f + sectionLabelPadBot;
curY += olFsz + 2.0f * dp + sectionLabelPadBot;
} else {
curY += sectionGap * 0.4f;
if (nSeparators < 4) separatorY[nSeparators++] = curY;
@@ -653,7 +654,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
fx.drawShimmer(dl, indMin, indMax, btnRnd);
fx.drawGradientBorderShift(dl, indMin, indMax, btnRnd);
}
DrawGlassCutout(dl, indMin, indMax, btnRnd, 1.5f);
DrawGlassCutout(dl, indMin, indMax, btnRnd, 1.5f * dp);
DrawGlassBevelButton(dl, indMin, indMax, btnRnd, btnDepth, 18);
buttonRects.push_back({indMin, indMax, btnRnd});
}
@@ -684,7 +685,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
bool itemHasBadge =
(item.page == NavPage::History && status.unconfirmedTxCount > 0) ||
(item.page == NavPage::Mining && status.miningActive) ||
(item.page == NavPage::Peers && status.peerCount > 0) ||
(item.page == NavPage::Chat && status.chatUnreadCount > 0);
float badgeReserve = 0.0f;
if (itemHasBadge) {
@@ -732,8 +732,6 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
badgeCol = Warning(); badgeTextCol = OnWarning();
} else if (item.page == NavPage::Mining && status.miningActive) {
dotOnly = true; badgeCol = Success();
} else if (item.page == NavPage::Peers && status.peerCount > 0) {
badgeCount = status.peerCount;
} else if (item.page == NavPage::Chat && status.chatUnreadCount > 0) {
badgeCount = status.chatUnreadCount;
}

View File

@@ -93,7 +93,7 @@ public:
// Arrow
{
float arrowCX = ImGui::GetContentRegionAvail().x * 0.5f;
ImGui::SetCursorPosX(arrowCX - 8.0f);
ImGui::SetCursorPosX(arrowCX - 8.0f * dp);
ImFont* iconFont = Type().iconMed();
float fsz = ScaledFontSize(iconFont);
ImVec2 pos = ImGui::GetCursorScreenPos();
@@ -312,7 +312,8 @@ private:
ImGui::Spacing();
ImGui::Spacing();
float btnW = 120.0f;
const float dp = Layout::dpiScale();
float btnW = 120.0f * dp;
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - btnW) * 0.5f);
if (TactileButton(TR("close"), ImVec2(btnW, 0))) {
s_open = false;

View File

@@ -264,7 +264,7 @@ void RenderSharedAddressList(App* app, float listH, float availW,
} else if (rows.empty()) {
float cw = ImGui::GetContentRegionAvail().x;
float ch = ImGui::GetContentRegionAvail().y;
if (ch < 60) ch = 60;
if (ch < 60.0f * dp) ch = 60.0f * dp;
const char* emptyMsg = addr_search[0] ? TR("no_addresses_match") : TR("no_addresses_yet");
ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg);
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);

View File

@@ -302,9 +302,9 @@ static void RenderBalanceClassic(App* app)
float cardPadLg = (classicPadOverride >= 0.0f) ? classicPadOverride : Layout::spacingLg();
// Card height: must fit the Market card's content (overline + price + 24h)
const float ovGap = S.drawElement("tabs.balance", "overline-value-gap").sizeOr(6.0f);
const float valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f);
const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f);
const float ovGap = S.drawElement("tabs.balance", "overline-value-gap").sizeOr(6.0f) * dp;
const float valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f) * dp;
const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f) * dp;
float marketContentH = cardPadLg
+ ovFont->LegacySize + ovGap
+ sub1->LegacySize + 2.0f * dp
@@ -323,7 +323,7 @@ static void RenderBalanceClassic(App* app)
// Helper: draw accent stripe on left edge, clipped to card rounded corners.
// We draw a full-size rounded rect (left corners only) and clip it to the
// stripe width so the shape itself follows the card rounding.
const float accentW = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f);
const float accentW = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f) * dp;
auto drawAccent = [&](const ImVec2& cMin, const ImVec2& cMax, ImU32 col) {
dl->PushClipRect(cMin, ImVec2(cMin.x + accentW, cMax.y), true);
dl->AddRectFilled(cMin, cMax, col, cardSpec.rounding,
@@ -446,7 +446,7 @@ static void RenderBalanceClassic(App* app)
ImU32 bd = WithAlpha(fg, 90);
snprintf(buf, sizeof(buf), "%s %s",
TR("data_stale_prefix"), timeAgo(state.last_balance_update).c_str());
ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd);
ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd, ImVec2(4.0f * dp, 2.0f * dp));
// Hover → explain what stale means and how old the data actually is.
if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + pillSz.x, cy + pillSz.y)))
Tooltip("%s", TR("data_stale_tooltip"));
@@ -456,7 +456,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
}
}
@@ -487,7 +487,7 @@ static void RenderBalanceClassic(App* app)
{
float privPct = (s_dispTotal > 1e-9)
? (float)(s_dispShielded / s_dispTotal * 100.0) : 0.0f;
snprintf(buf, sizeof(buf), "%.0f%% of total · %d Z-addr",
snprintf(buf, sizeof(buf), TR("baltab_pct_of_total_zaddr"),
privPct, (int)state.z_addresses.size());
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
WithAlpha(Success(), 160), buf);
@@ -498,8 +498,8 @@ static void RenderBalanceClassic(App* app)
snprintf(buf, sizeof(buf), "+%.4f", state.unconfirmed_balance);
ImVec2 ts = capFont->CalcTextSizeA(
capFont->LegacySize, 10000, 0, buf);
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f);
float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f);
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f) * dp;
float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f) * dp;
ImVec2 bMin(cMax.x - ts.x - bp * 3,
cMin.y + cardPadLg);
ImVec2 bMax(cMax.x - bp, bMin.y + ts.y + bp);
@@ -513,7 +513,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow + click to Receive
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::Receive);
@@ -543,7 +543,7 @@ static void RenderBalanceClassic(App* app)
OnSurfaceMedium(), DRAGONX_TICKER);
cy += sub1->LegacySize + valGap;
snprintf(buf, sizeof(buf), "%d T-addresses",
snprintf(buf, sizeof(buf), TR("baltab_t_addresses_count"),
(int)state.t_addresses.size());
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
OnSurfaceDisabled(), buf);
@@ -551,7 +551,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow + click to Receive
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::Receive);
@@ -589,7 +589,7 @@ static void RenderBalanceClassic(App* app)
[](unsigned char c){ return (char)std::toupper(c); });
float textW = std::max(pSz.x + tickGap + usdSz.x,
ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, marketLabel.c_str()).x);
float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f);
float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f) * dp;
float sparkLeft = cx + textW + sparkGap;
float sparkRight = cMax.x - cardPadLg;
@@ -611,7 +611,7 @@ static void RenderBalanceClassic(App* app)
bool pos = market.change_24h >= 0;
ImU32 chgCol = pos ? Success()
: Error();
snprintf(buf, sizeof(buf), "%s%.1f%% 24h",
snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"),
pos ? "+" : "", market.change_24h);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx, cy), chgCol, buf);
@@ -633,7 +633,7 @@ static void RenderBalanceClassic(App* app)
// Hover glow + click to Market
if (material::IsRectHovered(cMin, cMax)) {
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::Market);
@@ -790,20 +790,20 @@ static void RenderBalanceDonut(App* app) {
ImFont* capFont = Type().caption();
ImFont* body2 = Type().body2();
float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f);
float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f);
float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f);
float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f);
float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f) * dp;
float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f) * dp;
float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f) * dp;
float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f) * dp;
// Shielded legend
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Success());
snprintf(buf, sizeof(buf), "Shielded %.8f", s_dispShielded);
snprintf(buf, sizeof(buf), TR("baltab_shielded_amount"), s_dispShielded);
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Success(), buf);
legendY += capFont->LegacySize + legendLineGap;
// Transparent legend
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Warning());
snprintf(buf, sizeof(buf), "Transparent %.8f", s_dispTransparent);
snprintf(buf, sizeof(buf), TR("baltab_transparent_amount"), s_dispTransparent);
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Warning(), buf);
legendY += capFont->LegacySize + legendSectionGap;
@@ -811,15 +811,15 @@ static void RenderBalanceDonut(App* app) {
const auto& market = state.market;
if (market.price_usd > 0) {
if (market.price_usd >= 0.01)
snprintf(buf, sizeof(buf), "Market: $%.4f", market.price_usd);
snprintf(buf, sizeof(buf), TR("baltab_market_price_4dp"), market.price_usd);
else
snprintf(buf, sizeof(buf), "Market: $%.8f", market.price_usd);
snprintf(buf, sizeof(buf), TR("baltab_market_price_8dp"), market.price_usd);
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
OnSurfaceMedium(), buf);
legendY += capFont->LegacySize + 4 * dp;
bool pos = market.change_24h >= 0;
snprintf(buf, sizeof(buf), "%s%.1f%% 24h", pos ? "+" : "", market.change_24h);
snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"), pos ? "+" : "", market.change_24h);
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
pos ? Success() : Error(), buf);
}
@@ -946,7 +946,7 @@ static void RenderBalanceConsolidated(App* app) {
float divY = cardMin.y + cardH * S.drawElement("tabs.balance.consolidated", "divider-y-ratio").sizeOr(0.55f);
dl->AddLine(ImVec2(cardMin.x + pad, divY), ImVec2(cardMax.x - pad, divY),
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.consolidated", "divider-alpha").sizeOr(20.0f)),
S.drawElement("tabs.balance.consolidated", "divider-thickness").sizeOr(1.0f));
S.drawElement("tabs.balance.consolidated", "divider-thickness").sizeOr(1.0f) * dp);
// Bottom half: proportion bars
float barY = divY + Layout::spacingSm();
@@ -1122,7 +1122,7 @@ static void RenderBalanceDashboard(App* app) {
// Accent stripe — clipped to tile rounded corners
{
float aw = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f);
float aw = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f) * dp;
dl->PushClipRect(tMin, ImVec2(tMin.x + aw, tMax.y), true);
dl->AddRectFilled(tMin, tMax, tiles[i].accent, tileSpec.rounding,
ImDrawFlags_RoundCornersLeft);
@@ -1159,7 +1159,7 @@ static void RenderBalanceDashboard(App* app) {
// Click
if (material::IsRectHovered(tMin, tMax)) {
dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
tileSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
tileSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0))
app->setCurrentPage(tiles[i].nav);
@@ -1208,7 +1208,7 @@ static void RenderBalanceVerticalStack(App* app) {
// Font-content floor per row: icon + label + value must fit
float vstackRowFontFloor = std::max(body2->LegacySize, capFont->LegacySize)
+ Layout::spacingSm() * 2;
float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f);
float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f) * dp;
float vstackFontFloor = vstackRowFontFloor * 4 + rowGap * 3;
float vstackCardH = S.drawElement("tabs.balance.vertical-stack", "card-height").size;
float stackH;
@@ -1238,10 +1238,10 @@ static void RenderBalanceVerticalStack(App* app) {
};
RowInfo rowInfos[4] = {
{"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f},
{"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio},
{"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio},
{"Market", ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f},
{TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f},
{TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio},
{TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio},
{TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f},
};
for (int i = 0; i < 4; i++) {
@@ -1296,7 +1296,7 @@ static void RenderBalanceVerticalStack(App* app) {
// Proportion bar (for shielded/transparent rows — fills gap between label and amount)
if (i == 1 || i == 2) {
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
float barGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f);
float barGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f) * dp;
float barPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f);
float barH = std::max(
S.drawElement("tabs.balance.vertical-stack", "bar-min-height").sizeOr(3.0f),
@@ -1326,8 +1326,8 @@ static void RenderBalanceVerticalStack(App* app) {
// Sparkline in the gap between label and 24h change
if (state.market.price_history.size() >= 2) {
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
float sparkGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f);
float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f);
float sparkGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f) * dp;
float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f) * dp;
float sparkLeft = px + labelSz.x + sparkGap;
float sparkRight = chgX - sparkGap;
if (sparkLeft < sparkRight) {
@@ -1387,8 +1387,8 @@ static void RenderBalanceVertical2x2(App* app) {
ImFont* iconFont = Type().iconSmall();
// Font-content floor per row: caption text + vertical padding
float v2x2RowFontFloor = capFont->LegacySize + Layout::spacingSm() * 2;
float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f);
float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f);
float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f) * dp;
float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f) * dp;
float v2x2FontFloor = v2x2RowFontFloor * 2 + rowGap;
float cardHOverride = S.drawElement(cfgSec, "card-height").size;
float stackH;
@@ -1431,13 +1431,13 @@ static void RenderBalanceVertical2x2(App* app) {
CellInfo cells[2][2] = {
// Row 0: Total Balance (left), Shielded (right)
{
{"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false},
{"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
{TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false},
{TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
},
// Row 1: Market (left), Transparent (right)
{
{"Market", ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false},
{"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true},
{TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false},
{TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true},
},
};
@@ -1519,8 +1519,8 @@ static void RenderBalanceVertical2x2(App* app) {
// Sparkline between label and 24h change
if (state.market.price_history.size() >= 2) {
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, cell.label);
float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f);
float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f);
float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f) * dp;
float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f) * dp;
float sparkLeft = px + labelSz.x + sparkGap;
float sparkRight = chgX - sparkGap;
if (sparkLeft < sparkRight) {
@@ -1665,7 +1665,7 @@ static void RenderBalanceShield(App* app) {
ImVec2 needleTip(gaugeCx + cosf(needleAngle) * needleLen,
gaugeCy + sinf(needleAngle) * needleLen);
dl->AddLine(ImVec2(gaugeCx, gaugeCy), needleTip, gaugeCol,
S.drawElement("tabs.balance.shield", "needle-thickness").sizeOr(2.0f));
S.drawElement("tabs.balance.shield", "needle-thickness").sizeOr(2.0f) * dp);
// Center text: percentage
ImFont* sub1 = Type().subtitle1();
@@ -1956,7 +1956,7 @@ static void RenderBalanceTwoRow(App* app) {
}
RenderSyncBar(app, dl, vs);
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.two-row", "sync-gap").sizeOr(2.0f)));
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.two-row", "sync-gap").sizeOr(2.0f) * dp));
// Row 2: 3 mini-cards inline
{
@@ -1979,7 +1979,7 @@ static void RenderBalanceTwoRow(App* app) {
S.drawElement("tabs.balance.two-row", "mini-rounding-min").sizeOr(4.0f),
glassRound * S.drawElement("tabs.balance.two-row", "mini-rounding-ratio").sizeOr(0.5f));
ImFont* capFont = Type().caption();
float indicatorR = S.drawElement("tabs.balance.two-row", "indicator-radius").sizeOr(3.0f);
float indicatorR = S.drawElement("tabs.balance.two-row", "indicator-radius").sizeOr(3.0f) * dp;
int balDecimals = (int)S.drawElement("tabs.balance.two-row", "balance-decimals").sizeOr(4.0f);
float twoRowPadOverride = S.drawElement("tabs.balance.two-row", "card-padding").size;
float miniPad = (twoRowPadOverride >= 0.0f) ? twoRowPadOverride : Layout::spacingSm();
@@ -2054,8 +2054,8 @@ static void RenderBalanceTwoRow(App* app) {
// Sparkline between price and percentage
if (market.price_history.size() >= 2) {
float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f);
float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f);
float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f) * dp;
float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f) * dp;
float sparkLeft = cx + priceSz.x + sparkGap;
float sparkRightEdge = sparkRight - sparkGap;
if (sparkLeft < sparkRightEdge) {
@@ -2153,10 +2153,10 @@ static void RenderBalanceMinimal(App* app) {
{
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImVec2 sepPos = ImGui::GetCursorScreenPos();
float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f);
float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.0f);
float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f) * dp;
float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.0f) * dp;
float sepAlpha = S.drawElement("tabs.balance.minimal", "separator-alpha").sizeOr(25.0f);
float sepThick = S.drawElement("tabs.balance.minimal", "separator-thickness").sizeOr(1.0f);
float sepThick = S.drawElement("tabs.balance.minimal", "separator-thickness").sizeOr(1.0f) * dp;
float x = sepPos.x;
float endX = sepPos.x + availW;
while (x < endX) {

View File

@@ -59,7 +59,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
s_loading = false;
if (!error.empty()) {
s_error = "Error: " + error;
s_error = std::string(TR("grpa_error_prefix")) + error;
return;
}
@@ -84,7 +84,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
s_has_data = true;
} else {
s_error = "Invalid response from daemon";
s_error = TR("grpa_invalid_response_from_daemon");
}
}
@@ -125,7 +125,7 @@ void BlockInfoDialog::render(App* app)
// Current block info
if (state.sync.blocks > 0) {
ImGui::TextDisabled("(Current: %d)", state.sync.blocks);
ImGui::TextDisabled(TR("grpa_current_block_paren"), state.sync.blocks);
}
ImGui::SameLine();
@@ -152,7 +152,7 @@ void BlockInfoDialog::render(App* app)
rpc::RPCClient::TraceScope trace("Explorer / Block info");
auto hashResult = rpc->call("getblockhash", {height});
if (!hashResult.is_string()) {
error = "unexpected getblockhash result";
error = TR("grpa_unexpected_getblockhash_result");
} else {
block = rpc->call("getblock", {hashResult.get<std::string>()});
}

View File

@@ -144,7 +144,7 @@ private:
if (!s_bootstrap) {
s_state = State::Failed;
s_errorMsg = "Bootstrap not initialized";
s_errorMsg = TR("grpc_bootstrap_not_initialized");
return;
}
@@ -227,7 +227,7 @@ private:
s_state = State::Done;
} else {
s_errorMsg = finalProg.error;
if (s_errorMsg.empty()) s_errorMsg = "Bootstrap failed";
if (s_errorMsg.empty()) s_errorMsg = TR("grpc_bootstrap_failed");
s_state = State::Failed;
}
s_bootstrap.reset();

View File

@@ -425,9 +425,9 @@ void RenderChatSettingsPreview(App* app, float width) {
struct PMsg { const char* body; bool outgoing; bool startGroup; bool lastInGroup; std::string meta; };
const std::string peer = "Ava";
const PMsg msgs[] = {
{ u8"Did the payment go through? \U0001F642", false, true, true, peer + " " + t1 },
{ u8"Yep — just confirmed", true, true, false, std::string(TR("chat_you")) + " " + t2 },
{ u8"Sending the rest now \U0001F44D", true, false, true, std::string() },
{ TR("grpb_preview_msg_payment_through"), false, true, true, peer + " " + t1 },
{ TR("grpb_preview_msg_yep_confirmed"), true, true, false, std::string(TR("chat_you")) + " " + t2 },
{ TR("grpb_preview_msg_sending_rest"), true, false, true, std::string() },
};
const int N = 3;

View File

@@ -337,7 +337,7 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
float outputH = ComputeConsoleOutputHeight(
availHeight,
input_height,
schema::UI().drawElement("tabs.console", "output-min-height").size,
schema::UI().drawElement("tabs.console", "output-min-height").size * Layout::dpiScale(),
schema::UI().drawElement("tabs.console", "output-min-height-ratio").size);
ImDrawList* dlOut = ImGui::GetWindowDrawList();

View File

@@ -108,13 +108,13 @@ static const char* relativeTime(int64_t timestamp) {
int64_t diff = now - timestamp;
if (diff < 0) diff = 0;
if (diff < 60)
snprintf(buf, sizeof(buf), "%lld sec ago", (long long)diff);
snprintf(buf, sizeof(buf), TR("grpa_sec_ago"), (long long)diff);
else if (diff < 3600)
snprintf(buf, sizeof(buf), "%lld min ago", (long long)(diff / 60));
snprintf(buf, sizeof(buf), TR("grpa_min_ago"), (long long)(diff / 60));
else if (diff < 86400)
snprintf(buf, sizeof(buf), "%lld hr ago", (long long)(diff / 3600));
snprintf(buf, sizeof(buf), TR("grpa_hr_ago"), (long long)(diff / 3600));
else
snprintf(buf, sizeof(buf), "%lld days ago", (long long)(diff / 86400));
snprintf(buf, sizeof(buf), TR("grpa_days_ago"), (long long)(diff / 86400));
return buf;
}
@@ -1216,7 +1216,7 @@ static void renderBlockDetailModal(App* app) {
txDL->AddRectFilled(rowStart,
ImVec2(rowStart.x + txContentW, rowStart.y + txRowH),
WithAlpha(OnSurface(), 10),
S.drawElement("tabs.explorer", "row-rounding").size);
S.drawElement("tabs.explorer", "row-rounding").size * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", txid.c_str());
}
@@ -1315,7 +1315,7 @@ static void renderBlockDetailModal(App* app) {
}
if (s_detail_txids.size() > 100) {
snprintf(buf, sizeof(buf), "... showing first 100 of %d", (int)s_detail_txids.size());
snprintf(buf, sizeof(buf), TR("grpa_showing_first_100_of"), (int)s_detail_txids.size());
ImGui::TextDisabled("%s", buf);
}
}

View File

@@ -0,0 +1,115 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "faq_content.h"
namespace dragonx {
namespace ui {
namespace faq {
// ── Wallet group ───────────────────────────────────────────────────────────
const std::vector<FaqSubcategory>& walletFaq()
{
static const std::vector<FaqSubcategory> kWallet = {
{ "faq_w_gs_title", {
{ "faq_w_gs_1_q", "faq_w_gs_1_a" },
{ "faq_w_gs_2_q", "faq_w_gs_2_a" },
{ "faq_w_gs_3_q", "faq_w_gs_3_a" },
{ "faq_w_gs_4_q", "faq_w_gs_4_a" },
}},
{ "faq_w_addr_title", {
{ "faq_w_addr_1_q", "faq_w_addr_1_a" },
{ "faq_w_addr_2_q", "faq_w_addr_2_a" },
{ "faq_w_addr_3_q", "faq_w_addr_3_a" },
{ "faq_w_addr_4_q", "faq_w_addr_4_a" },
}},
{ "faq_w_send_title", {
{ "faq_w_send_1_q", "faq_w_send_1_a" },
{ "faq_w_send_2_q", "faq_w_send_2_a" },
{ "faq_w_send_3_q", "faq_w_send_3_a" },
{ "faq_w_send_4_q", "faq_w_send_4_a" },
}},
{ "faq_w_bal_title", {
{ "faq_w_bal_1_q", "faq_w_bal_1_a" },
{ "faq_w_bal_2_q", "faq_w_bal_2_a" },
{ "faq_w_bal_3_q", "faq_w_bal_3_a" },
}},
{ "faq_w_sec_title", {
{ "faq_w_sec_1_q", "faq_w_sec_1_a" },
{ "faq_w_sec_2_q", "faq_w_sec_2_a" },
{ "faq_w_sec_3_q", "faq_w_sec_3_a" },
}},
{ "faq_w_seed_title", {
{ "faq_w_seed_1_q", "faq_w_seed_1_a" },
{ "faq_w_seed_2_q", "faq_w_seed_2_a" },
{ "faq_w_seed_3_q", "faq_w_seed_3_a" },
{ "faq_w_seed_4_q", "faq_w_seed_4_a" },
}},
{ "faq_w_chat_title", {
{ "faq_w_chat_1_q", "faq_w_chat_1_a" },
{ "faq_w_chat_2_q", "faq_w_chat_2_a" },
{ "faq_w_chat_3_q", "faq_w_chat_3_a" },
}},
{ "faq_w_set_title", {
{ "faq_w_set_1_q", "faq_w_set_1_a" },
{ "faq_w_set_2_q", "faq_w_set_2_a" },
{ "faq_w_set_3_q", "faq_w_set_3_a" },
{ "faq_w_set_4_q", "faq_w_set_4_a" },
}},
};
return kWallet;
}
// ── Daemon group (full-node only) ──────────────────────────────────────────
const std::vector<FaqSubcategory>& daemonFaq()
{
static const std::vector<FaqSubcategory> kDaemon = {
{ "faq_d_node_title", {
{ "faq_d_node_1_q", "faq_d_node_1_a" },
{ "faq_d_node_2_q", "faq_d_node_2_a" },
{ "faq_d_node_3_q", "faq_d_node_3_a" },
}},
{ "faq_d_sync_title", {
{ "faq_d_sync_1_q", "faq_d_sync_1_a" },
{ "faq_d_sync_2_q", "faq_d_sync_2_a" },
{ "faq_d_sync_3_q", "faq_d_sync_3_a" },
{ "faq_d_sync_4_q", "faq_d_sync_4_a" },
}},
{ "faq_d_mgmt_title", {
{ "faq_d_mgmt_1_q", "faq_d_mgmt_1_a" },
{ "faq_d_mgmt_2_q", "faq_d_mgmt_2_a" },
{ "faq_d_mgmt_3_q", "faq_d_mgmt_3_a" },
}},
{ "faq_d_upd_title", {
{ "faq_d_upd_1_q", "faq_d_upd_1_a" },
{ "faq_d_upd_2_q", "faq_d_upd_2_a" },
{ "faq_d_upd_3_q", "faq_d_upd_3_a" },
}},
{ "faq_d_mine_title", {
{ "faq_d_mine_1_q", "faq_d_mine_1_a" },
{ "faq_d_mine_2_q", "faq_d_mine_2_a" },
{ "faq_d_mine_3_q", "faq_d_mine_3_a" },
{ "faq_d_mine_4_q", "faq_d_mine_4_a" },
}},
{ "faq_d_net_title", {
{ "faq_d_net_1_q", "faq_d_net_1_a" },
{ "faq_d_net_2_q", "faq_d_net_2_a" },
}},
{ "faq_d_perf_title", {
{ "faq_d_perf_1_q", "faq_d_perf_1_a" },
{ "faq_d_perf_2_q", "faq_d_perf_2_a" },
}},
{ "faq_d_trbl_title", {
{ "faq_d_trbl_1_q", "faq_d_trbl_1_a" },
{ "faq_d_trbl_2_q", "faq_d_trbl_2_a" },
{ "faq_d_trbl_3_q", "faq_d_trbl_3_a" },
{ "faq_d_trbl_4_q", "faq_d_trbl_4_a" },
}},
};
return kDaemon;
}
} // namespace faq
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,41 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// FAQ content model. The FAQ screen is data-driven: this header exposes the two
// top-level groups (Wallet, Daemon) as ordered lists of subcategories, each a list
// of question/answer entries. Every string is an i18n KEY (looked up with TR at
// render time), so wording + translations live in src/util/i18n.cpp + res/lang/*.json
// and never require touching UI code. Add a Q&A by appending a {qKey,aKey} pair here
// and its two strings to loadBuiltinEnglish().
#pragma once
#include <vector>
namespace dragonx {
namespace ui {
namespace faq {
// One question and its answer, both i18n keys. The answer may contain "\n\n"
// paragraph breaks; it is rendered wrapped. Keep answers free of printf specifiers
// (%d/%s/…) — the i18n layer rejects translations whose format signature drifts.
struct FaqEntry {
const char* questionKey;
const char* answerKey;
};
// A named group of questions. titleKey is an i18n key for the subcategory header.
struct FaqSubcategory {
const char* titleKey;
std::vector<FaqEntry> entries;
};
// The two top-level groups. daemonFaq() is full-node material and is only shown when
// the build supports full-node lifecycle actions (see App::supportsFullNodeLifecycleActions()).
const std::vector<FaqSubcategory>& walletFaq();
const std::vector<FaqSubcategory>& daemonFaq();
} // namespace faq
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,201 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "faq_dialog.h"
#include "faq_content.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../util/text_format.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "../schema/ui_schema.h"
#include "../layout.h"
#include "../material/type.h"
#include "../material/colors.h"
#include "../material/draw_helpers.h"
#include "imgui.h"
#include <algorithm>
#include <string>
#include <unordered_map>
#include <vector>
namespace dragonx {
namespace ui {
namespace {
// Persists across frames (the dialog is re-entered each frame while open). Group 0 = Wallet,
// 1 = Daemon. `expanded` keys are FaqEntry::questionKey (stable string literals from faq_content).
struct FaqDialogState {
int group = 0;
char search[128] = "";
std::unordered_map<std::string, bool> expanded;
};
FaqDialogState s_faq;
bool entryMatches(const faq::FaqEntry& e, const char* query)
{
return util::containsIgnoreCase(TR(e.questionKey), query) ||
util::containsIgnoreCase(TR(e.answerKey), query);
}
// One answer body: wrapped, muted. Shared by the collapsible (non-search) and search paths.
void renderAnswer(const char* answerKey)
{
ImGui::Indent(Layout::spacingMd());
ImGui::PushFont(material::Type().body2());
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
ImGui::TextWrapped("%s", TR(answerKey));
ImGui::PopStyleColor();
ImGui::PopFont();
ImGui::Unindent(Layout::spacingMd());
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
}
} // namespace
void RenderFaqDialog(App* app, bool* p_open)
{
auto& S = schema::UI();
auto win = S.window("dialogs.faq");
const float dp = Layout::dpiScale();
const bool daemonAvailable = app && app->supportsFullNodeLifecycleActions();
if (!daemonAvailable) s_faq.group = 0; // no Daemon tab in lite builds
// Floating "BlurFloat" modal, matching the Wallets dialog: live-blur backdrop, no boxed card, a
// plain heading (no ✕ — a Close button sits at the bottom). Roomy fixed width; height capped to the
// viewport so the content flexes + scrolls on small / HiDPI screens.
const float vpH = ImGui::GetMainViewport()->Size.y;
const float wantH = (win.height > 0 ? win.height : 640.0f) * dp;
material::OverlayDialogSpec spec;
spec.title = TR("faq_title");
spec.p_open = p_open;
spec.style = material::OverlayStyle::BlurFloat;
spec.cardWidth = (win.width > 0 ? win.width : 860.0f);
spec.cardHeight = std::min(wantH, vpH * 0.86f) / dp;
spec.idSuffix = "faq";
if (!material::BeginOverlayDialog(spec)) {
return;
}
// Esc closes (ImGui consumes Esc itself while the search box is being edited, so this only
// fires when the field isn't capturing it).
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) *p_open = false;
// Subtitle under the plain heading, matching the Wallets dialog's intro caption.
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(), TR("faq_intro"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
const float contentW = ImGui::GetContentRegionAvail().x;
// ── Group tabs: Wallet | Daemon ──
{
const int nTabs = daemonAvailable ? 2 : 1;
const float gap = ImGui::GetStyle().ItemSpacing.x;
const float tabW = (contentW - gap * (nTabs - 1)) / static_cast<float>(nTabs);
auto tab = [&](const char* label, int idx) {
const bool active = (s_faq.group == idx);
if (active) {
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::Primary()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnPrimary()));
}
if (material::TactileButton(label, ImVec2(tabW, 0))) s_faq.group = idx;
if (active) ImGui::PopStyleColor(2);
};
tab(TR("faq_group_wallet"), 0);
if (daemonAvailable) { ImGui::SameLine(); tab(TR("faq_group_daemon"), 1); }
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// ── Search ──
ImGui::SetNextItemWidth(contentW);
ImGui::InputTextWithHint("##FaqSearch", TR("faq_search_hint"), s_faq.search, sizeof(s_faq.search));
const bool searching = s_faq.search[0] != '\0';
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// ── Scrollable Q&A body ── (reserve room for the Close button footer below)
const float footerH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingMd();
float bodyH = ImGui::GetContentRegionAvail().y - footerH;
if (bodyH < 80.0f * dp) bodyH = 80.0f * dp;
// Inner padding gives the content breathing room and, on the right, a clear gap to the LEFT of the
// scrollbar (WindowPadding.x is exactly that gap); the scrollbar itself is made chunkier than the
// app default. NoScrollWithMouse + ApplySmoothScroll gives the wheel eased scrolling, matching the
// Wallets dialog / Settings page (ApplySmoothScroll owns the wheel input and lerps ScrollY).
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingMd(), Layout::spacingXs()));
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 16.0f * dp);
ImGui::BeginChild("##FaqScroll", ImVec2(0, bodyH), false,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollWithMouse);
material::ApplySmoothScroll();
ImDrawList* dl = ImGui::GetWindowDrawList();
const auto& groups = (s_faq.group == 1 && daemonAvailable) ? faq::daemonFaq() : faq::walletFaq();
bool anyShown = false;
bool firstSection = true;
for (const auto& subcat : groups) {
// Collect the entries visible under the current search.
std::vector<const faq::FaqEntry*> visible;
for (const auto& e : subcat.entries) {
if (!searching || entryMatches(e, s_faq.search)) visible.push_back(&e);
}
if (visible.empty()) continue;
// Section break: generous space above every section after the first, so topic groups read as
// clearly separated bands rather than one uniform list.
if (!firstSection) ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
firstSection = false;
anyShown = true;
// Section header — accent overline + a thin full-width rule beneath it, anchoring the group
// above its (brighter, normal-case) question rows.
material::Type().textColored(material::TypeStyle::Overline,
material::Primary(), TR(subcat.titleKey));
{
const ImVec2 rp = ImGui::GetCursorScreenPos();
const float rw = ImGui::GetContentRegionAvail().x;
dl->AddLine(ImVec2(rp.x, rp.y + dp), ImVec2(rp.x + rw, rp.y + dp),
material::Divider(), 1.0f * dp);
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
for (const auto* e : visible) {
const float rowW = ImGui::GetContentRegionAvail().x;
if (searching) {
// Search results: show question + answer directly (no collapsing).
ImGui::PushFont(material::Type().subtitle2());
ImGui::TextWrapped("%s", TR(e->questionKey));
ImGui::PopFont();
renderAnswer(e->answerKey);
} else {
bool& exp = s_faq.expanded[e->questionKey];
std::string id = std::string("##faq_") + e->questionKey;
material::CollapsibleHeader(dl, id.c_str(), TR(e->questionKey), exp, rowW,
material::Type().subtitle2(), material::OnSurface());
if (exp) renderAnswer(e->answerKey);
}
}
}
if (!anyShown) {
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
ImGui::TextWrapped("%s", TR("faq_no_results"));
ImGui::PopStyleColor();
}
ImGui::EndChild();
ImGui::PopStyleVar(2); // ScrollbarSize, WindowPadding
// Close button footer (BlurFloat has no ✕ in the heading).
const float closeW = 120.0f * dp;
material::BeginOverlayDialogFooter(closeW, false);
if (material::TactileButton(TR("close"), ImVec2(closeW, 0))) *p_open = false;
material::EndOverlayDialog();
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,18 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
namespace dragonx {
class App;
namespace ui {
// Renders the Help & FAQ modal. Call every frame while *p_open is true; the dialog
// clears *p_open itself on close (✕ / click-outside / Esc). Content is data-driven
// (see faq_content.h) and grouped into Wallet and Daemon tabs; the Daemon tab is
// hidden on builds without full-node lifecycle support.
void RenderFaqDialog(App* app, bool* p_open);
} // namespace ui
} // namespace dragonx

View File

@@ -173,7 +173,7 @@ void KeyExportDialog::render(App* app)
s_key = found;
s_show_key = wantViewing; // viewing keys are less sensitive
} else {
s_error = r.ok ? std::string("Key not available for this address") : r.error;
s_error = r.ok ? std::string(TR("grpc_key_not_available")) : r.error;
}
wallet::secureWipeLiteSecret(found);
s_fetching = false;
@@ -240,14 +240,15 @@ void KeyExportDialog::render(App* app)
ImGui::TextDisabled("%s", TR("key_export_click_retrieve"));
} else {
// Key fetched. Layout: [ key text (click-to-copy) + Show/Hide below ] [ QR | square ].
const float gap = 12.0f;
const float dp = Layout::dpiScale();
const float gap = 12.0f * dp;
const float avail = ImGui::GetContentRegionAvail().x;
// Larger, responsive QR: ~30% of the content width, clamped to a comfortable range.
float qrSize = avail * 0.30f;
if (qrSize < 200.0f) qrSize = 200.0f;
if (qrSize > 340.0f) qrSize = 340.0f;
if (qrSize < 200.0f * dp) qrSize = 200.0f * dp;
if (qrSize > 340.0f * dp) qrSize = 340.0f * dp;
float keyW = avail - qrSize - gap;
const bool sideBySide = keyW >= 200.0f; // too narrow -> stack the QR under the key
const bool sideBySide = keyW >= 200.0f * dp; // too narrow -> stack the QR under the key
if (!sideBySide) keyW = avail;
// Chunk for readability, but keep a Bech32 HRP (e.g. "secret-extended-key-main") intact

View File

@@ -1580,7 +1580,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
}
} else {
const char* status = market.price_loading ? TR("market_price_loading") : TR("market_price_unavailable");
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx0, cy + 10), OnSurfaceDisabled(), status);
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx0, cy + 10 * dp), OnSurfaceDisabled(), status);
if (!market.price_loading && !market.price_error.empty()) {
std::string errorText = market.price_error;
float maxErrorW = cardMax.x - cx0 - Layout::spacingLg();
@@ -1590,7 +1590,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
}
if (errorText.size() < market.price_error.size()) errorText += "...";
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx0, cy + 10 + sub1->LegacySize + Layout::spacingXs()),
ImVec2(cx0, cy + 10 * dp + sub1->LegacySize + Layout::spacingXs()),
Warning(), errorText.c_str());
}
}
@@ -1874,7 +1874,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
: (tk == ticks - 1) ? plotRight - lblSz.x
: xpos - lblSz.x * 0.5f;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(lx, plotBottom + 4), OnSurfaceDisabled(), tlbl);
ImVec2(lx, plotBottom + 4 * mktDp), OnSurfaceDisabled(), tlbl);
}
}

View File

@@ -153,7 +153,12 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
}
};
ImFont* stepFont = Type().iconSmall();
const float fieldH = capFont->LegacySize + 6.0f * dp;
// Match the -/+ button height to the InputInt's actual frame height (GetFrameHeight ==
// fontSize + 2*FramePadding.y, evaluated with the same font/style the input renders with,
// since nothing pushes a font between here and the InputInt below). This keeps the buttons
// the SAME height as the number box and top-aligned with it at sy — previously fieldH was
// capFont+6px, shorter than the box, so they sat misaligned.
const float fieldH = ImGui::GetFrameHeight();
const float sideW = fieldH; // square -/+ buttons
const float fieldW = 46.0f * dp;
const float g = 2.0f * dp;
@@ -544,7 +549,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
s_benchConfirm = true;
char msg[128];
snprintf(msg, sizeof(msg),
"Benchmark takes ~%ds and interrupts mining. Click again to start.",
TR("grpc_benchmark_takes_secs"),
(int)(s_benchmark.totalEstimatedSecs() + 0.5f));
Notifications::instance().warning(msg);
} else {

View File

@@ -220,7 +220,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
if (estActive)
snprintf(estVal, sizeof(estVal), "~%.4f", estDaily);
else
snprintf(estVal, sizeof(estVal), "N/A");
snprintf(estVal, sizeof(estVal), "%s", TR("grpc_na"));
// Disclose in pool mode that Est. Daily is a rough solo-equivalent (before the pool fee), so the
// number isn't silently mismatched to its plain "Est. Daily" label. (M-05)
const char* estSub = (s_pool_mode && estActive) ? TR("mining_est_daily_pool_sub") : nullptr;
@@ -475,7 +475,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
else if (totalRAM > 0)
snprintf(sysBuf, sizeof(sysBuf), "-- / %.0f GB", totalRAM / 1024.0);
else
snprintf(sysBuf, sizeof(sysBuf), "N/A");
snprintf(sysBuf, sizeof(sysBuf), "%s", TR("grpc_na"));
float sysTextW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, sysBuf).x;
float sysTextX = barX + barW - textPadX - sysTextW;

View File

@@ -326,7 +326,7 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
? it->second.feePercent
: kp.feePercent;
if (feePct >= 0.0)
snprintf(right, sizeof(right), "%s %s%% fee", hrStr.c_str(),
snprintf(right, sizeof(right), TR("grpc_hashrate_fee"), hrStr.c_str(),
FormatFeePercent(feePct).c_str());
else
snprintf(right, sizeof(right), "%s", hrStr.c_str());
@@ -421,9 +421,10 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
// centered ~1000dp band inside the panel's padded content region; the
// glass panel itself still fills rightW, leftover -> side margin.
const float rightContentW = rightW - pad * 2.0f;
const float chartBandW = std::min(rightContentW, 1000.0f * dp);
const float chartBandX = rightMin.x + pad
+ std::max(0.0f, (rightContentW - chartBandW) * 0.5f);
// Fill the panel's content width so the chart extends across the whole panel on a wide window.
// (Previously capped at ~1000dp and centered, which left large empty margins either side.)
const float chartBandW = rightContentW;
const float chartBandX = rightMin.x + pad;
// Right panel: live log (if toggled + available) else the sparkline.
if (showLogView) {

View File

@@ -263,8 +263,7 @@ static void RenderMiningTabContent(App* app)
}
if (benchmarkUpdate.inconclusive) {
Notifications::instance().warning(
"Benchmark inconclusive: no hashrate samples were recorded. "
"Check the pool connection and try again.");
TR("grpc_benchmark_inconclusive"));
}
}

View File

@@ -380,7 +380,7 @@ void RenderPeersTab(App* app)
if (tlsCount == totalPeers) {
ImFont* iconFont = Type().iconSmall();
ImVec2 txtSize = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf);
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4, valY), Success(), ICON_MD_CHECK);
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4 * dp, valY), Success(), ICON_MD_CHECK);
}
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
@@ -695,7 +695,7 @@ void RenderPeersTab(App* app)
ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size * dp);
ImVec2 pillMax(dirX + dirSz.x + S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + capFont->LegacySize + S.drawElement("tabs.peers", "dir-pill-y-bottom").size * dp);
dl->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size * dp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2), dirFg, dirLabel);
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2 * dp), dirFg, dirLabel);
}
{
@@ -745,9 +745,9 @@ void RenderPeersTab(App* app)
ImU32 tlsBg = WithAlpha(Success(), 25);
ImU32 tlsFg = WithAlpha(Success(), 200);
ImVec2 tlsMin(badgeX, cy2);
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2);
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2 * dp);
dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size * dp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4, cy2 + 1), tlsFg, "TLS");
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4 * dp, cy2 + 1 * dp), tlsFg, "TLS");
} else {
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2),
WithAlpha(Error(), 140), TR("peers_no_tls"));

View File

@@ -259,11 +259,11 @@ static void RenderAddressDropdown(App* app, float width) {
snprintf(buf, sizeof(buf), "%s %s (%s) \xe2\x80\x94 %.8f %s%s",
tag, lblIt->second.c_str(), trunc.c_str(),
addr.balance, DRAGONX_TICKER,
isNew ? " [NEW]" : "");
isNew ? TR("grpb_new_badge_suffix") : "");
} else {
snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s%s",
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER,
isNew ? " [NEW]" : "");
isNew ? TR("grpb_new_badge_suffix") : "");
}
ImGui::PushID(static_cast<int>(i));
@@ -272,9 +272,9 @@ static void RenderAddressDropdown(App* app, float width) {
s_cached_qr_data.clear(); // Force QR regeneration
}
if (ImGui::IsItemHovered()) {
material::Tooltip("%s\nBalance: %.8f %s%s",
material::Tooltip(TR("grpb_tooltip_address_balance"),
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
isCurrent ? "\n(selected)" : "");
isCurrent ? TR("grpb_selected_suffix") : "");
}
ImGui::PopID();
}
@@ -716,17 +716,17 @@ void RenderReceiveTab(App* app)
ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text);
float ss = iconW * 0.5f;
float cx = startX + ss;
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size;
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size * cardDp;
float al = ss * S.drawElement("tabs.receive", "swap-icon-arrow-length-ratio").size;
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size;
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size * cardDp;
float ay1 = cy - ag;
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size);
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
dl->AddTriangleFilled(
ImVec2(cx + al, ay1),
ImVec2(cx + al - hss, ay1 - hss),
ImVec2(cx + al - hss, ay1 + hss), iconCol);
float ay2 = cy + ag;
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size);
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
dl->AddTriangleFilled(
ImVec2(cx - al, ay2),
ImVec2(cx - al + hss, ay2 - hss),
@@ -744,8 +744,8 @@ void RenderReceiveTab(App* app)
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size;
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size;
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size;
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size * cardDp;
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size * cardDp;
struct Preset { const char* label; double amount; };
Preset presets[] = {
@@ -873,7 +873,7 @@ void RenderReceiveTab(App* app)
RenderQRCode(s_qr_texture, qrSize);
} else {
ImGui::Dummy(ImVec2(qrSize, qrSize));
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size,
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size * cardDp,
qrPanelMin.y + totalQrSize * 0.5f);
dl->AddText(capFont, capFont->LegacySize, textPos,
OnSurfaceDisabled(), TR("qr_unavailable"));

View File

@@ -364,7 +364,8 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
if (suggestions.empty()) return;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor(schema::UI().drawElement("tabs.send", "suggestion-bg-color").color)));
float sugH = std::min((float)suggestions.size() * schema::UI().drawElement("tabs.send", "suggestion-row-height").size + schema::UI().drawElement("tabs.send", "suggestion-list-padding").size, schema::UI().drawElement("tabs.send", "suggestion-max-height").size);
const float dp = Layout::dpiScale();
float sugH = std::min((float)suggestions.size() * schema::UI().drawElement("tabs.send", "suggestion-row-height").size * dp + schema::UI().drawElement("tabs.send", "suggestion-list-padding").size * dp, schema::UI().drawElement("tabs.send", "suggestion-max-height").size * dp);
ImGui::BeginChild(childId, ImVec2(width, sugH), true);
for (size_t si = 0; si < suggestions.size(); si++) {
int sugTrunc = (int)schema::UI().drawElement("tabs.send", "suggestion-trunc-len").size;
@@ -387,13 +388,14 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
// ============================================================================
static void RenderFeeTierSelector(const char* suffix = "") {
auto& S = schema::UI();
const float dp = Layout::dpiScale();
const char* feeLabels[] = { TR("send_fee_low"), TR("send_fee_normal"), TR("send_fee_high") };
const double feeValues[] = { FEE_LOW, FEE_NORMAL, FEE_HIGH };
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "fee-rounding").size);
for (int fi = 0; fi < 3; fi++) {
if (fi > 0) ImGui::SameLine(0, S.drawElement("tabs.send", "fee-tier-gap").size);
if (fi > 0) ImGui::SameLine(0, S.drawElement("tabs.send", "fee-tier-gap").size * dp);
bool active = (s_fee_tier == fi);
if (active) {
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "fee-tier-active-bg-alpha").size)));
@@ -515,7 +517,7 @@ static void RenderAmountBar(ImDrawList* dl, double available, float innerW,
// Max button — use caption font to fit bar height
ImGui::SameLine(0, gap);
char maxId[32];
snprintf(maxId, sizeof(maxId), "Max%s", suffix);
snprintf(maxId, sizeof(maxId), "%s%s", TR("grpb_max"), suffix);
if (TactileButton(maxId, ImVec2(maxBtnW, barH), capFont)) {
s_amount = maxAmount;
s_send_max = true;
@@ -976,6 +978,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
bool is_valid_address, double available,
const char* suffix = "") {
auto& S = schema::UI();
const float dp = Layout::dpiScale();
const auto& state = app->getWalletState();
double total = s_amount + s_fee;
// Block spending from a view-only source (imported viewing key, no spending key) — it would only
@@ -1065,12 +1068,12 @@ static void RenderActionButtons(App* app, float width, float vScale,
if (ImGui::BeginPopup(confirmClearId)) {
ImGui::Text("%s", TR("send_clear_fields"));
ImGui::Spacing();
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size, 0), S.resolveFont("button"))) {
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size * dp, 0), S.resolveFont("button"))) {
ClearFormWithUndo();
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size, 0), S.resolveFont("button"))) {
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size * dp, 0), S.resolveFont("button"))) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
@@ -1086,7 +1089,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-bg-alpha").size)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-hover-alpha").size)));
char undoId[32];
snprintf(undoId, sizeof(undoId), "Undo Clear%s", suffix);
snprintf(undoId, sizeof(undoId), "%s%s", TR("grpb_undo_clear"), suffix);
if (TactileButton(undoId, ImVec2(width, btnH), S.resolveFont("button"))) {
RestoreFormSnapshot();
Notifications::instance().info(TR("send_form_restored"));

View File

@@ -216,7 +216,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
if (!skin.valid) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::BeginDisabled(true);
std::string label = skin.name + " (invalid)";
std::string label = skin.name + TR("swin_invalid_suffix");
ImGui::Selectable(label.c_str(), false);
ImGui::EndDisabled();
ImGui::PopStyleColor();
@@ -261,7 +261,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
ImGui::PushFont(material::Type().iconSmall());
if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) {
skinMgr.refresh();
Notifications::instance().info("Theme list refreshed");
Notifications::instance().info(TR("swin_theme_list_refreshed"));
}
ImGui::PopFont();
if (ImGui::IsItemHovered()) {
@@ -425,14 +425,14 @@ void RenderSettingsWindow(App* app, bool* p_open)
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
if (error.empty()) {
std::string version = result.value("version", "unknown");
std::string msg = "Connection successful!\ndragonxd version: " + version;
std::string msg = std::string(TR("swin_connection_successful")) + version;
Notifications::instance().success(msg);
} else {
Notifications::instance().error("Connection failed: " + error);
Notifications::instance().error(std::string(TR("swin_connection_failed")) + error);
}
});
} else {
Notifications::instance().error("RPC client not initialized");
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
}
}
@@ -455,15 +455,15 @@ void RenderSettingsWindow(App* app, bool* p_open)
if (error.empty()) {
int start = result.value("start_height", 0);
int end = result.value("stop_height", 0);
std::string msg = "Rescan started from block " + std::to_string(start) +
" to " + std::to_string(end);
std::string msg = std::string(TR("swin_rescan_started_from_block")) + std::to_string(start) +
TR("swin_rescan_to") + std::to_string(end);
Notifications::instance().success(msg);
} else {
Notifications::instance().error("Rescan failed: " + error);
Notifications::instance().error(std::string(TR("swin_rescan_failed")) + error);
}
});
} else {
Notifications::instance().error("RPC client not initialized");
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
}
}
ImGui::TextDisabled(" %s", TR("settings_rescan_desc"));
@@ -503,9 +503,9 @@ void RenderSettingsWindow(App* app, bool* p_open)
if (doConfirm) {
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
if (util::Platform::deleteFile(ztx_file)) {
Notifications::instance().success("Z-transaction history cleared");
Notifications::instance().success(TR("swin_ztx_history_cleared"));
} else {
Notifications::instance().info("No history file found");
Notifications::instance().info(TR("swin_no_history_file_found"));
}
s_confirm_clear_ztx = false;
}
@@ -568,7 +568,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
// Save/Cancel buttons
if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
saveSettingsFromUI(app->settings());
Notifications::instance().success("Settings saved");
Notifications::instance().success(TR("swin_settings_saved"));
*p_open = false;
}
ImGui::SameLine();

View File

@@ -124,7 +124,7 @@ void TransactionDetailsDialog::render(App* app)
ImGui::SetNextItemWidth(txidInput.width); // negative = fill, reserving |width| px for the Copy button (a content-region margin, not raw px — must NOT be dpi-scaled)
ImGui::InputText("##TxID", txid_buf, sizeof(txid_buf), ImGuiInputTextFlags_ReadOnly);
ImGui::SameLine();
if (material::StyledButton("Copy##TxID", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
if (material::StyledButton((std::string(TR("grpb_copy")) + "##TxID").c_str(), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
ImGui::SetClipboardText(tx.txid.c_str());
}
@@ -150,7 +150,7 @@ void TransactionDetailsDialog::render(App* app)
ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
}
ImGui::SameLine();
if (material::StyledButton("Copy##Addr", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
if (material::StyledButton((std::string(TR("grpb_copy")) + "##Addr").c_str(), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
ImGui::SetClipboardText(tx.address.c_str());
}
}

View File

@@ -263,10 +263,10 @@ void RenderTransactionsTab(App* app)
int idx_map[] = {-1, 1, 0, 2};
int idx = idx_map[type_filter];
float xOff = idx * (cardW + cardGap);
ImVec2 acMin(origin.x + xOff, origin.y + cardH - 3);
ImVec2 acMin(origin.x + xOff, origin.y + cardH - 3 * vs);
ImVec2 acMax(origin.x + xOff + cardW, origin.y + cardH);
ImU32 acCol = (type_filter == 1) ? redCol : (type_filter == 2) ? greenCol : goldCol;
dl->AddRectFilled(acMin, acMax, acCol, 2.0f);
dl->AddRectFilled(acMin, acMax, acCol, 2.0f * hs);
}
ImGui::Dummy(ImVec2(availWidth, cardH));

View File

@@ -100,7 +100,7 @@ public:
const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title
+ Type().caption()->LegacySize + Layout::spacingSm() // intro + gap
+ ctrlRow + Layout::spacingSm(); // sort control row + gap
const float padV = 48.0f; // content-child padding (top+bottom) + margin
const float padV = 48.0f * dp; // content-child padding (top+bottom) + margin
// Size to content, but cap at a viewport fraction: with many wallets on a small / HiDPI
// screen the content-sized card could exceed the window (the framework would clip a too-tall
// card, footer and all). When capped, the wallet list flexes + scrolls and the controls