feat(ui): implement UI/UX audit — i18n, HiDPI, theme, destructive-action & overflow fixes

Implements all 26 confirmed UI/UX audit findings plus the 4 dashboard/timeline
tile labels. Verified across full-node + Lite + Windows builds (ctest green) and
an adversarial diff review (one market-tab delete regression caught + fixed).

i18n coverage:
- Balance hero/quick-actions/toasts + dashboard & timeline tiles (Total Balance,
  Shielded, Transparent, Quick Send, Quick Receive, Click to open, Market)
- Send: Review Send / Cancel / Paste / view-only tooltip / memo byte counter
- Settings: Lite lifecycle errors, plaintext-RPC security warning, 8 toasts
- Mining pool-payout tooltip
- Resolve daemon_update_title double-assignment collision (new daemon_update_prompt_title)
- 23 new keys translated into all 8 locales; CJK subset font rebuilt

HiDPI: DPI-scale the Send amount bar, mining thread-tile clamp bounds, receive
loading skeleton, and sidebar notification-badge insets.

Light theme: theme-aware material::SurfaceOverlay() for balance bar tracks and
row-hover highlights; contacts active-pill foreground uses OnPrimary().

Destructive actions: arm/confirm for portfolio-group delete, saved pool/worker
remove, and avatar-image delete.

Interaction/overflow: route balance star/eye buttons and the console fold-toggle
through popup-safe guards; clip peer addr/subver; truncate the balance custom
label; measure Settings tool-button widths; mining stepper disabled-state
feedback; +/- stepper buttons restyled to match the thread tiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-18 17:54:15 -05:00
co-authored by Claude Opus 4.8
parent 0de44569b7
commit e24ca015d1
24 changed files with 599 additions and 148 deletions
+20
View File
@@ -116,6 +116,26 @@ inline ImVec4 WarningVec4() { return ImGui::ColorConvertU32ToFloat4(Warni
// Convenience Functions for Common Patterns
// ============================================================================
/**
* @brief Theme-aware translucent overlay for tracks / hover fills / dividers.
*
* A raw white overlay (IM_COL32(255,255,255,a)) reads on dark skins but vanishes
* on light/pastel skins (white-on-white). This picks a dark overlay on light
* themes and a white overlay on dark themes so the alpha reads either way.
* (Self-contained luminance check so colors.h stays free of draw_helpers.h.)
*
* @param alpha 0-255 opacity of the overlay
*/
inline ImU32 SurfaceOverlay(int alpha)
{
ImU32 bg = Background();
float r = ((bg >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
float g = ((bg >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
float b = ((bg >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
bool light = (0.299f * r + 0.587f * g + 0.114f * b) > 0.5f;
return light ? IM_COL32(0, 0, 0, alpha) : IM_COL32(255, 255, 255, alpha);
}
/**
* @brief Get color with applied state overlay
*
+40 -15
View File
@@ -309,7 +309,7 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) {
std::string trimmedPath(s_settingsState.lite_wallet_path);
const auto first = trimmedPath.find_first_not_of(" \t\r\n");
if (first == std::string::npos) {
s_settingsState.lite_lifecycle_status = "Enter a wallet path";
s_settingsState.lite_lifecycle_status = TR("lite_enter_wallet_path");
s_settingsState.lite_lifecycle_summary.clear();
return;
}
@@ -320,8 +320,9 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) {
if (input.request.operation == wallet::LiteWalletLifecycleOperation::RestoreFromSeed) {
const int words = liteSeedWordCount(s_settingsState.lite_restore_seed);
if (!dragonx::util::isCompleteRecoveryPhrase(words)) {
s_settingsState.lite_lifecycle_status =
"Enter all 24 seed words to restore (got " + std::to_string(words) + ")";
char seedBuf[128];
snprintf(seedBuf, sizeof(seedBuf), TR("lite_enter_all_seed_words"), words);
s_settingsState.lite_lifecycle_status = seedBuf;
s_settingsState.lite_lifecycle_summary.clear();
return;
}
@@ -353,7 +354,7 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) {
// Rejected before any thread launched (wallet already open, an attempt in flight, or no
// usable server). The controller's status carries the reason.
s_settingsState.lite_lifecycle_status = lite->status().message.empty()
? "Could not start the operation"
? TR("lite_could_not_start")
: lite->status().message;
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
}
@@ -364,7 +365,7 @@ static void evaluateLiteLifecycleRequestFromPageState(App* app) {
// failed to load / rollout-disabled). The live path above returns when a backend is
// present, so reaching here means there is nothing to run.
s_settingsState.lite_lifecycle_summary.clear();
s_settingsState.lite_lifecycle_status = "Lite wallet backend unavailable";
s_settingsState.lite_lifecycle_status = TR("lite_backend_unavailable");
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
}
@@ -819,7 +820,7 @@ void RenderSettingsPage(App* app) {
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
schema::SkinManager::instance().refresh();
Notifications::instance().info("Theme list refreshed");
Notifications::instance().info(TR("settings_theme_refreshed"));
}
if (ImGui::IsItemHovered()) {
material::Tooltip(TR("tt_scan_themes"),
@@ -1064,7 +1065,7 @@ void RenderSettingsPage(App* app) {
ImGui::SameLine();
if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
schema::SkinManager::instance().refresh();
Notifications::instance().info("Theme list refreshed");
Notifications::instance().info(TR("settings_theme_refreshed"));
}
if (ImGui::IsItemHovered()) {
material::Tooltip(TR("tt_scan_themes"),
@@ -1386,6 +1387,30 @@ void RenderSettingsPage(App* app) {
float bw = (contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow;
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(100.0f);
bw = std::max(minBtnW, bw);
// Grow the column so the longest translated label (e.g. German) isn't clipped inside
// ImGui::Button. Measure every label with the actual button font (LegacySize is already
// DPI-scaled — don't scale it again) and add the button's own FramePadding on both sides;
// if that exceeds bw, drop to fewer columns rather than overflow the card width.
{
ImFont* toolsFont = S.resolveFont("button");
if (!toolsFont) toolsFont = Type().button();
const char* toolLabels[] = {
TR("settings_address_book"), TR("settings_validate_address"),
TR("settings_request_payment"), TR("settings_shield_mining"),
TR("settings_merge_to_address"), TR("settings_clear_ztx"),
};
float widestLabel = 0.0f;
for (const char* lbl : toolLabels)
widestLabel = std::max(widestLabel,
toolsFont->CalcTextSizeA(toolsFont->LegacySize, FLT_MAX, 0, lbl).x);
const float needW = widestLabel + ImGui::GetStyle().FramePadding.x * 2.0f
+ 8.0f * Layout::dpiScale();
// Shed columns until the widest label fits (or we're down to a single column).
while (btnsPerRow > 1 &&
((contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow) < needW)
--btnsPerRow;
bw = std::max({minBtnW, (contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow, needW});
}
if (TactileButton(TR("settings_address_book"), ImVec2(bw, 0), S.resolveFont("button")))
app->setCurrentPage(ui::NavPage::Contacts); // now a top-level tab
@@ -2225,7 +2250,7 @@ void RenderSettingsPage(App* app) {
if (s_settingsState.rpc_plaintext_remote) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
ImGui::PushTextWrapPos(sectionOrigin.x + contentW);
ImGui::TextWrapped("Remote RPC is using plaintext HTTP. Add rpctls=1 to DRAGONX.conf if your daemon supports TLS.");
ImGui::TextWrapped("%s", TR("rpc_plaintext_remote_warning"));
ImGui::PopTextWrapPos();
ImGui::PopStyleColor();
}
@@ -2367,14 +2392,14 @@ void RenderSettingsPage(App* app) {
try {
rpc::RPCClient::TraceScope trace("Settings / Test connection");
rpc->call("getinfo");
return []() { Notifications::instance().success("RPC connection OK"); };
return []() { Notifications::instance().success(TR("settings_rpc_ok")); };
} catch (const std::exception& e) {
std::string err = e.what();
return [err]() { Notifications::instance().error("RPC error: " + err); };
return [err]() { Notifications::instance().error(std::string(TR("settings_rpc_error_prefix")) + err); };
}
});
} else {
Notifications::instance().warning("Not connected to daemon");
Notifications::instance().warning(TR("settings_not_connected"));
}
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_test_conn"));
@@ -2587,14 +2612,14 @@ void RenderSettingsPage(App* app) {
if (aboutGrid) ImGui::Dummy(ImVec2(0, Layout::spacingXs())); else ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TrId("save_settings", "about_save").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
saveSettingsPageState(app->settings());
Notifications::instance().success("Settings saved");
Notifications::instance().success(TR("settings_saved"));
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_settings"));
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TrId("reset_to_defaults", "about_reset").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
if (app->settings()) {
loadSettingsPageState(app->settings());
Notifications::instance().info("Settings reloaded from disk");
Notifications::instance().info(TR("settings_reloaded"));
}
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_reset_settings"));
@@ -2913,9 +2938,9 @@ void RenderSettingsPage(App* app) {
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("settings_ztx_cleared"));
} else {
Notifications::instance().info("No history file found");
Notifications::instance().info(TR("settings_ztx_not_found"));
}
s_settingsState.confirm_clear_ztx = false;
}
+4 -2
View File
@@ -722,8 +722,10 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
if (badgeCount > 0 || dotOnly) {
float badgeR = dotOnly ? badgeRadiusDot : badgeRadiusNumber;
float bx = indMax.x - badgeR - 6.0f;
float by = indMin.y + badgeR + 5.0f;
float badgeInsetX = sde("badge-inset-x", 6.0f);
float badgeInsetY = sde("badge-inset-y", 5.0f);
float bx = indMax.x - badgeR - badgeInsetX;
float by = indMin.y + badgeR + badgeInsetY;
dl->AddCircleFilled(ImVec2(bx, by), badgeR, badgeCol);
if (!dotOnly && showLabels) {
char buf[16];
+22 -8
View File
@@ -466,7 +466,8 @@ void RenderSharedAddressList(App* app, float listH, float availW,
{
const auto& starRect = rowLayout.favoriteButton;
ImVec2 bMin(starRect.x, starRect.y), bMax(starRect.x + starRect.width, starRect.y + starRect.height);
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
// material::IsRectHovered so the button inherits overlay/popup input blocking
bool bHov = material::IsRectHovered(bMin, bMax);
dl->AddRectFilled(bMin, bMax, row.favorite ? favGoldFill : (bHov ? btnFillHov : btnFill), btnRound);
dl->AddRect(bMin, bMax, row.favorite ? favGoldBorder : (bHov ? btnBorderHov : btnBorder), btnRound, 0, 1.0f * dp);
ImFont* iconFont = Type().iconSmall();
@@ -492,7 +493,8 @@ void RenderSharedAddressList(App* app, float listH, float availW,
if (showEye) {
const auto& eyeRect = rowLayout.visibilityButton;
ImVec2 bMin(eyeRect.x, eyeRect.y), bMax(eyeRect.x + eyeRect.width, eyeRect.y + eyeRect.height);
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
// material::IsRectHovered so the button inherits overlay/popup input blocking
bool bHov = material::IsRectHovered(bMin, bMax);
dl->AddRectFilled(bMin, bMax, bHov ? btnFillHov : btnFill, btnRound);
dl->AddRect(bMin, bMax, bHov ? btnBorderHov : btnBorder, btnRound, 0, 1.0f * dp);
ImFont* iconFont = Type().iconSmall();
@@ -539,12 +541,24 @@ void RenderSharedAddressList(App* app, float listH, float availW,
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, miningTag);
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
// User label next to type
// User label next to type — clip to the gap before the right-aligned
// balance so a long custom label can't overrun the balance on this line
// (mirrors the address-line width guard below).
if (!row.label.empty()) {
float typeLabelW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeBuf).x;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(labelX + typeLabelW + Layout::spacingLg(), cy),
OnSurfaceMedium(), row.label.c_str());
float userLabelX = labelX + typeLabelW + Layout::spacingLg();
// Balance is drawn right-aligned at contentRight; recompute its left edge here.
char balBufPeek[32];
snprintf(balBufPeek, sizeof(balBufPeek), "%.8f", addr.balance);
float balW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBufPeek).x;
float userLabelAvailW = (contentRight - balW - Layout::spacingMd()) - userLabelX;
std::string userLabel = material::TruncateToWidth(
row.label, capFont, capFont->LegacySize, userLabelAvailW);
if (userLabelAvailW > 0.0f) {
dl->AddText(capFont, capFont->LegacySize,
ImVec2(userLabelX, cy),
OnSurfaceMedium(), userLabel.c_str());
}
}
}
@@ -841,7 +855,7 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
float rowW = ImGui::GetContentRegionAvail().x;
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
if (material::IsRectHovered(rowPos, rowEnd)) {
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
dl->AddRectFilled(rowPos, rowEnd, SurfaceOverlay(15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::History);
@@ -868,7 +882,7 @@ void RenderSyncBar(App* app, ImDrawList* dl, float vs) {
ImVec2 barPos = ImGui::GetCursorScreenPos();
dl->AddRectFilled(barPos,
ImVec2(barPos.x + barW, barPos.y + barH),
IM_COL32(255, 255, 255, 15), 1.0f * dp);
SurfaceOverlay(15), 1.0f * dp);
dl->AddRectFilled(barPos,
ImVec2(barPos.x + barW * prog, barPos.y + barH),
WithAlpha(Warning(), 200), 1.0f * dp);
+75 -31
View File
@@ -31,6 +31,7 @@
#include "imgui.h"
#include <toml++/toml.hpp>
#include <algorithm>
#include <cctype>
#include <cstring>
#include <ctime>
#include <cmath>
@@ -196,7 +197,9 @@ void RenderBalanceTab(App* app)
for (const auto& l : allLayouts) {
if (l.id == layoutId) { displayName = l.name; break; }
}
Notifications::instance().info("Layout: " + displayName);
char layoutToast[128];
snprintf(layoutToast, sizeof(layoutToast), TR("balance_layout_switched"), displayName.c_str());
Notifications::instance().info(layoutToast);
}
}
}
@@ -359,8 +362,11 @@ static void RenderBalanceClassic(App* app)
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.classic", "logo-opacity").sizeOr(180.0f)));
}
std::string totalLabelUpper = TR("total_balance_label");
std::transform(totalLabelUpper.begin(), totalLabelUpper.end(), totalLabelUpper.begin(),
[](unsigned char c){ return (char)std::toupper(c); });
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy),
OnSurfaceMedium(), "TOTAL BALANCE");
OnSurfaceMedium(), totalLabelUpper.c_str());
cy += ovFont->LegacySize + ovGap;
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
@@ -387,7 +393,7 @@ static void RenderBalanceClassic(App* app)
// Sync progress or mining indicator (whichever fits)
if (state.sync.syncing && state.sync.headers > 0) {
float pct = static_cast<float>(state.sync.verification_progress) * 100.0f;
snprintf(buf, sizeof(buf), "Syncing %.1f%%", pct);
snprintf(buf, sizeof(buf), TR("balance_syncing_pct"), pct);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
Warning(), buf);
@@ -401,7 +407,7 @@ static void RenderBalanceClassic(App* app)
dl->PushClipRect(ImVec2(cMin.x, barTop), cMax, true);
// Background track
dl->AddRectFilled(cMin, cMax,
IM_COL32(255, 255, 255, 15), cardSpec.rounding);
SurfaceOverlay(15), cardSpec.rounding);
// Progress fill — additional horizontal clip
float progRight = cMin.x + (cMax.x - cMin.x) * prog;
dl->PushClipRect(ImVec2(cMin.x, barTop), ImVec2(progRight, cMax.y), true);
@@ -418,7 +424,11 @@ static void RenderBalanceClassic(App* app)
dl->AddCircleFilled(ImVec2(cx + 4 * dp, cy + capFont->LegacySize * 0.5f),
S.drawElement("tabs.balance.classic", "mining-dot-radius").sizeOr(3.0f), mineCol);
double hr = state.mining.localHashrate;
snprintf(buf, sizeof(buf), " Mining %s", FormatHashrate(hr).c_str());
// Leading indent clears the mining dot drawn at cx+4dp; the text
// itself starts at cx+12dp so keep the two spaces for spacing parity.
char mineFmt[64];
snprintf(mineFmt, sizeof(mineFmt), " %s", TR("balance_mining_rate"));
snprintf(buf, sizeof(buf), mineFmt, FormatHashrate(hr).c_str());
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx + 12 * dp, cy),
WithAlpha(Success(), 200), buf);
@@ -574,15 +584,18 @@ static void RenderBalanceClassic(App* app)
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, "USD");
// Measure widest text line to determine sparkline left edge
std::string marketLabel = TR("market");
std::transform(marketLabel.begin(), marketLabel.end(), marketLabel.begin(),
[](unsigned char c){ return (char)std::toupper(c); });
float textW = std::max(pSz.x + tickGap + usdSz.x,
ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, "MARKET").x);
ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, marketLabel.c_str()).x);
float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f);
float sparkLeft = cx + textW + sparkGap;
float sparkRight = cMax.x - cardPadLg;
// Left side: label + price + 24h change
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy),
OnSurfaceMedium(), "MARKET");
OnSurfaceMedium(), marketLabel.c_str());
cy += ovFont->LegacySize + ovGap;
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy),
@@ -682,7 +695,7 @@ static void RenderBalanceDonut(App* app) {
else
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.donut", "hero-pad-ratio").sizeOr(8.0f) * vs));
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("total_balance_label"));
ImGui::Dummy(ImVec2(0, 2 * dp));
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
ImFont* heroFont = Type().h2();
@@ -947,10 +960,13 @@ static void RenderBalanceConsolidated(App* app) {
// Shielded bar
float shieldX = cardMin.x + pad;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(shieldX, barY), Success(), "SHIELDED");
std::string shieldLabelUpper = TR("shielded");
std::transform(shieldLabelUpper.begin(), shieldLabelUpper.end(), shieldLabelUpper.begin(),
[](unsigned char c){ return (char)std::toupper(c); });
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(shieldX, barY), Success(), shieldLabelUpper.c_str());
barY += ovFont->LegacySize + 4 * dp;
dl->AddRectFilled(ImVec2(shieldX, barY), ImVec2(shieldX + halfW, barY + barH),
IM_COL32(255, 255, 255, 15), barH * 0.5f);
SurfaceOverlay(15), barH * 0.5f);
dl->AddRectFilled(ImVec2(shieldX, barY), ImVec2(shieldX + halfW * shieldRatio, barY + barH),
WithAlpha(Success(), 180), barH * 0.5f);
barY += barH + 2 * dp;
@@ -960,10 +976,13 @@ static void RenderBalanceConsolidated(App* app) {
// Transparent bar
float transX = cardMin.x + pad * 2 + halfW;
barY = divY + Layout::spacingSm();
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(transX, barY), Warning(), "TRANSPARENT");
std::string transLabelUpper = TR("transparent");
std::transform(transLabelUpper.begin(), transLabelUpper.end(), transLabelUpper.begin(),
[](unsigned char c){ return (char)std::toupper(c); });
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(transX, barY), Warning(), transLabelUpper.c_str());
barY += ovFont->LegacySize + 4 * dp;
dl->AddRectFilled(ImVec2(transX, barY), ImVec2(transX + halfW, barY + barH),
IM_COL32(255, 255, 255, 15), barH * 0.5f);
SurfaceOverlay(15), barH * 0.5f);
dl->AddRectFilled(ImVec2(transX, barY), ImVec2(transX + halfW * transRatio, barY + barH),
WithAlpha(Warning(), 180), barH * 0.5f);
barY += barH + 2 * dp;
@@ -982,7 +1001,7 @@ static void RenderBalanceConsolidated(App* app) {
dl->PushClipRect(ImVec2(cardMin.x, syncBarTop), cardMax, true);
// Background track
dl->AddRectFilled(cardMin, cardMax,
IM_COL32(255, 255, 255, 15), glassRound);
SurfaceOverlay(15), glassRound);
// Progress fill — additional horizontal clip
float progRight = cardMin.x + (cardMax.x - cardMin.x) * prog;
dl->PushClipRect(ImVec2(cardMin.x, syncBarTop), ImVec2(progRight, cardMax.y), true);
@@ -1075,11 +1094,20 @@ static void RenderBalanceDashboard(App* app) {
snprintf(shBuf, sizeof(shBuf), "%.8f", s_dispShielded);
snprintf(trBuf, sizeof(trBuf), "%.8f", s_dispTransparent);
// Localized captions — raw dl->AddText (no auto-uppercase), so uppercase to preserve the caption look.
auto upperTR = [](const char* key) {
std::string s = TR(key);
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return (char)std::toupper(c); });
return s;
};
std::string lblShielded = upperTR("shielded"), lblTransparent = upperTR("transparent");
std::string lblQuickSend = upperTR("quick_send"), lblQuickReceive = upperTR("quick_receive");
TileInfo tiles[4] = {
{"SHIELDED", shBuf, S.resolveColor("var(--accent-shielded)", Success()), ICON_MD_SHIELD, NavPage::Receive, false},
{"TRANSPARENT", trBuf, S.resolveColor("var(--accent-transparent)", Warning()), ICON_MD_CIRCLE, NavPage::Receive, false},
{"QUICK SEND", "Send", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_MADE, NavPage::Send, true},
{"QUICK RECEIVE", "Receive", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_RECEIVED, NavPage::Receive, true},
{lblShielded.c_str(), shBuf, S.resolveColor("var(--accent-shielded)", Success()), ICON_MD_SHIELD, NavPage::Receive, false},
{lblTransparent.c_str(), trBuf, S.resolveColor("var(--accent-transparent)", Warning()), ICON_MD_CIRCLE, NavPage::Receive, false},
{lblQuickSend.c_str(), "Send", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_MADE, NavPage::Send, true},
{lblQuickReceive.c_str(), "Receive", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_RECEIVED, NavPage::Receive, true},
};
for (int i = 0; i < 4; i++) {
@@ -1125,7 +1153,7 @@ static void RenderBalanceDashboard(App* app) {
tiles[i].accent, tiles[i].value);
} else {
dl->AddText(capFont, capFont->LegacySize, ImVec2(tMin.x + tilePad, py),
OnSurfaceMedium(), "Click to open");
OnSurfaceMedium(), TR("tile_click_to_open"));
}
// Click
@@ -1279,7 +1307,7 @@ static void RenderBalanceVerticalStack(App* app) {
float barW = barRight - barLeft;
float barY = rowPos.y + (rowH - barH) * 0.5f;
dl->AddRectFilled(ImVec2(barLeft, barY), ImVec2(barRight, barY + barH),
IM_COL32(255, 255, 255, 15), barH * 0.5f);
SurfaceOverlay(15), barH * 0.5f);
dl->AddRectFilled(ImVec2(barLeft, barY),
ImVec2(barLeft + barW * rowInfos[i].ratio, barY + barH),
WithAlpha(rowInfos[i].accent, 180), barH * 0.5f);
@@ -1473,7 +1501,7 @@ static void RenderBalanceVertical2x2(App* app) {
float barX = cellMax.x - amtSz.x - rowPad - barW - Layout::spacingSm();
float barY = cellMin.y + (rowH - barH) * 0.5f;
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH),
IM_COL32(255, 255, 255, 15), barH * 0.5f);
SurfaceOverlay(15), barH * 0.5f);
dl->AddRectFilled(ImVec2(barX, barY),
ImVec2(barX + barW * cell.ratio, barY + barH),
WithAlpha(cell.accent, 180), barH * 0.5f);
@@ -1551,7 +1579,7 @@ static void RenderBalanceShield(App* app) {
else
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance", "compact-hero-pad").sizeOr(8.0f) * vs));
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("total_balance_label"));
ImGui::Dummy(ImVec2(0, 2 * dp));
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
ImFont* heroFont = Type().h2();
@@ -1665,13 +1693,19 @@ static void RenderBalanceShield(App* app) {
float infoY = panelMin.y + shieldPad;
ImFont* ovFont = Type().overline();
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Success(), "SHIELDED");
std::string shieldLabelUpper = TR("shielded");
std::transform(shieldLabelUpper.begin(), shieldLabelUpper.end(), shieldLabelUpper.begin(),
[](unsigned char c){ return (char)std::toupper(c); });
std::string transLabelUpper = TR("transparent");
std::transform(transLabelUpper.begin(), transLabelUpper.end(), transLabelUpper.begin(),
[](unsigned char c){ return (char)std::toupper(c); });
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Success(), shieldLabelUpper.c_str());
infoY += ovFont->LegacySize + 2 * dp;
snprintf(buf, sizeof(buf), "%.8f", s_dispShielded);
dl->AddText(capFont, capFont->LegacySize, ImVec2(infoX, infoY), Success(), buf);
infoY += capFont->LegacySize + 6 * dp;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Warning(), "TRANSPARENT");
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Warning(), transLabelUpper.c_str());
infoY += ovFont->LegacySize + 2 * dp;
snprintf(buf, sizeof(buf), "%.8f", s_dispTransparent);
dl->AddText(capFont, capFont->LegacySize, ImVec2(infoX, infoY), Warning(), buf);
@@ -1727,7 +1761,7 @@ static void RenderBalanceTimeline(App* app) {
else
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance", "compact-hero-pad").sizeOr(8.0f) * vs));
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("total_balance_label"));
ImGui::Dummy(ImVec2(0, 2 * dp));
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
ImFont* heroFont = Type().h2();
@@ -1816,10 +1850,16 @@ static void RenderBalanceTimeline(App* app) {
spec.rounding = glassRound;
struct SumCard { const char* label; ImU32 col; double val; bool isMoney; };
auto upperTR = [](const char* key) {
std::string s = TR(key);
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return (char)std::toupper(c); });
return s;
};
std::string cShielded = upperTR("shielded"), cTransparent = upperTR("transparent"), cMarket = upperTR("market");
SumCard cards[3] = {
{"SHIELDED", Success(), s_dispShielded, false},
{"TRANSPARENT", Warning(), s_dispTransparent, false},
{"MARKET", Primary(), state.market.price_usd, true},
{cShielded.c_str(), Success(), s_dispShielded, false},
{cTransparent.c_str(), Warning(), s_dispTransparent, false},
{cMarket.c_str(), Primary(), state.market.price_usd, true},
};
for (int i = 0; i < 3; i++) {
ImVec2 cMin(origin.x + i * (cardW + cGap), origin.y);
@@ -1887,13 +1927,13 @@ static void RenderBalanceTwoRow(App* app) {
ImFont* capFont = Type().caption();
if (state.sync.syncing && state.sync.headers > 0) {
float pct = static_cast<float>(state.sync.verification_progress) * 100.0f;
snprintf(buf, sizeof(buf), "Syncing %.1f%%", pct);
snprintf(buf, sizeof(buf), TR("balance_syncing_pct"), pct);
Type().textColored(TypeStyle::Caption, Warning(), buf);
ImGui::SameLine();
}
if (state.mining.generate) {
double hr = state.mining.localHashrate;
snprintf(buf, sizeof(buf), "Mining %s", FormatHashrate(hr).c_str());
snprintf(buf, sizeof(buf), TR("balance_mining_rate"), FormatHashrate(hr).c_str());
Type().textColored(TypeStyle::Caption, WithAlpha(Success(), 200), buf);
ImGui::SameLine();
}
@@ -1902,11 +1942,15 @@ static void RenderBalanceTwoRow(App* app) {
float btnW = S.drawElement("tabs.balance.two-row", "action-btn-width").sizeOr(80.0f);
float rightEdge = ImGui::GetWindowWidth() - Layout::spacingLg();
ImGui::SameLine(rightEdge - btnW * 2 - Layout::spacingSm());
if (TactileButton("Send", ImVec2(btnW, 0), S.resolveFont("button"))) {
// Stable ## ids keep the button identity fixed across translations.
char sendBtn[64], recvBtn[64];
snprintf(sendBtn, sizeof(sendBtn), "%s##tworow-send", TR("send"));
snprintf(recvBtn, sizeof(recvBtn), "%s##tworow-receive", TR("receive"));
if (TactileButton(sendBtn, ImVec2(btnW, 0), S.resolveFont("button"))) {
app->setCurrentPage(NavPage::Send);
}
ImGui::SameLine();
if (TactileButton("Receive", ImVec2(btnW, 0), S.resolveFont("button"))) {
if (TactileButton(recvBtn, ImVec2(btnW, 0), S.resolveFont("button"))) {
app->setCurrentPage(NavPage::Receive);
}
}
+3
View File
@@ -1050,8 +1050,11 @@ void ConsoleTab::drawVisibleLines(float padX, float lineHeight, bool hasTextFilt
ImVec2(cx, cy + sz * 0.7f), triCol); // ▼ expanded
}
// Click anywhere in the gutter cell for this line's first row toggles the fold.
// Guard against a click that is actually dismissing the ConsoleContextMenu (or any
// popup) — the same popup guard the text-selection path uses (see mouse_in_output).
ImVec2 mp = ImGui::GetIO().MousePos;
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
!ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopup) &&
mp.x >= output_origin_.x - padX && mp.x < output_origin_.x &&
mp.y >= lineOrigin.y && mp.y < lineOrigin.y + lineHeight) {
pendingFoldToggle = i;
+18 -6
View File
@@ -40,6 +40,7 @@ static bool s_show_edit_dialog = false;
static bool s_show_contacts_settings = false; // Contacts customization modal (gear button)
static bool s_focus_edit_field = false; // focus the first field the frame the add/edit dialog opens
static int s_confirm_delete_idx = -1; // armed storage index; a 2nd Delete confirms
static int s_confirm_avatar_del_idx = -1; // armed avatar-library index; a 2nd badge click confirms the (irreversible) file delete
static char s_edit_label[128] = "";
static char s_edit_address[512] = "";
static char s_edit_notes[512] = "";
@@ -555,7 +556,7 @@ void RenderContactsTab(App* app)
sdl->AddRectFilled(ImVec2(cMin.x + 2.0f * dp, cMin.y + 2.0f * dp),
ImVec2(cMax.x - 2.0f * dp, cMax.y - 2.0f * dp),
material::WithAlpha(material::Primary(), 210), (segH - 4.0f * dp) * 0.5f);
ImU32 fg = active ? IM_COL32(255, 255, 255, 255) : (hov ? material::OnSurface() : material::OnSurfaceMedium());
ImU32 fg = active ? material::OnPrimary() : (hov ? material::OnSurface() : material::OnSurfaceMedium());
float igW = segIcoF->CalcTextSizeA(segIcoF->LegacySize, FLT_MAX, 0, segIco[i]).x;
float lbW = btnFont->CalcTextSizeA(btnFont->LegacySize, FLT_MAX, 0, segLbl[i]).x;
float gapI = 5.0f * dp;
@@ -747,12 +748,16 @@ void RenderContactsTab(App* app)
float dr = std::max(7.0f * dp, cell * 0.15f);
ImVec2 dcc(mx.x - dr - 3.0f * dp, mn.y + dr + 3.0f * dp);
bool dhov = ImGui::IsMouseHoveringRect(ImVec2(dcc.x-dr, dcc.y-dr), ImVec2(dcc.x+dr, dcc.y+dr));
const int libIdx = n - 1;
const bool delArmed = (s_confirm_avatar_del_idx == libIdx);
ImGui::PushID(n);
bool thumbClicked = ImGui::InvisibleButton("##avthumb", ImVec2(cell, cell));
bool thumbHov = ImGui::IsItemHovered();
ImGui::PopID();
if (thumbHov || sel || hov) { // draw the delete badge
gdl->AddCircleFilled(dcc, dr, dhov ? material::ReadableError() : IM_COL32(0, 0, 0, 175));
if (thumbHov || sel || hov || delArmed) { // draw the delete badge (persist while armed)
gdl->AddCircleFilled(dcc, dr, (dhov || delArmed) ? material::ReadableError() : IM_COL32(0, 0, 0, 175));
// While armed, ring the badge so the "click again to delete" state is unmistakable.
if (delArmed) gdl->AddCircle(dcc, dr + 1.5f * dp, material::ReadableError(), 0, 1.5f * dp);
ImFont* xf = material::Type().iconSmall();
float xsz = dr * 1.35f;
ImVec2 xs = xf->CalcTextSizeA(xsz, FLT_MAX, 0, ICON_MD_CLOSE);
@@ -760,11 +765,18 @@ void RenderContactsTab(App* app)
}
if (dhov) { // badge takes priority over selecting the thumbnail
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("delete"));
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) pendingDelete = n - 1;
// Two-stage confirm: the first click arms this badge; a second click on the SAME
// badge deletes the image file (fs::remove is irreversible — no undo).
material::Tooltip("%s", delArmed ? TR("address_book_confirm_delete") : TR("delete"));
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
if (delArmed) { pendingDelete = libIdx; s_confirm_avatar_del_idx = -1; }
else s_confirm_avatar_del_idx = libIdx; // arm; requires a 2nd deliberate click
}
} else {
if (thumbHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (thumbClicked) s_edit_avatar = "img:" + path;
// Any click that lands off this badge (selecting the thumbnail or elsewhere)
// disarms it, so a stale armed badge can't be confirmed by an unrelated click.
if (thumbClicked) { s_edit_avatar = "img:" + path; s_confirm_avatar_del_idx = -1; }
}
}
col = (col + 1) % cols;
+27 -4
View File
@@ -137,6 +137,12 @@ struct PfEditState {
};
static PfEditState s_pfEdit;
// Two-stage delete arm/confirm for the master-list per-row delete icon (mirrors the
// contacts tab's s_confirm_delete_idx): first click on a row's trash arms it (icon turns
// red); a second click on the SAME row within the loop confirms the erase. Hovering/clicking
// a different row, or clicking off the icons, disarms. -1 = nothing armed.
static int s_pfConfirmDelIdx = -1;
// The pure price-series math lives in data/market_series.h; the selected chart range is
// s_mkt.chartInterval (0=Live 1=1H 2=1D 3=1W 4=1M, session-scoped, default 1M).
@@ -837,6 +843,7 @@ static void RenderPortfolioEditor(App* app)
float rowH = 46.0f * dp;
float delSlot = 36.0f * dp; // reserved trailing space for the delete icon + margin
int clickedSel = -999, delRow = -1; // deferred: don't mutate `entries` mid-loop
bool armedThisFrame = false; // an arm-click also fires the row Selectable; don't let it disarm
for (int vi = 0; vi < (int)vis.size(); vi++) {
int i = vis[vi];
ImGui::PushID(i);
@@ -877,6 +884,7 @@ static void RenderPortfolioEditor(App* app)
// Per-row delete icon (larger, inset from the edge). Hit-tested manually so it is
// NOT an overlapping ImGui item over the row Selectable (which asserted on hover).
if (rowHov || selRow) {
bool armed = (s_pfConfirmDelIdx == i); // this row is arm-confirmed
ImFont* delFont = Type().iconMed();
float ds = delFont->LegacySize;
ImVec2 dc(rmx.x - Layout::spacingMd() - ds * 0.5f, rmn.y + rowH * 0.5f);
@@ -884,11 +892,20 @@ static void RenderPortfolioEditor(App* app)
bool dhov = ImGui::IsMouseHoveringRect(dmn, dmx);
if (dhov) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) delRow = i;
// Hovering a *different* row's trash disarms the previously armed one.
if (!armed && s_pfConfirmDelIdx >= 0) s_pfConfirmDelIdx = -1;
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
if (armed) delRow = i; // 2nd click on the armed row erases
else { s_pfConfirmDelIdx = i; // 1st click arms this row...
armedThisFrame = true; } // ...and must survive the row-select this same click triggers
}
}
ImVec2 isz = delFont->CalcTextSizeA(ds, FLT_MAX, 0, ICON_MD_DELETE_OUTLINE);
// A filled trash glyph while armed (vs outline) reinforces the recolor.
const char* delGlyph = armed ? ICON_MD_DELETE : ICON_MD_DELETE_OUTLINE;
ImU32 delCol = armed ? ReadableError() : (dhov ? Error() : OnSurfaceMedium());
ImVec2 isz = delFont->CalcTextSizeA(ds, FLT_MAX, 0, delGlyph);
mdl->AddText(delFont, ds, ImVec2(dc.x - isz.x * 0.5f, dc.y - isz.y * 0.5f),
dhov ? Error() : OnSurfaceMedium(), ICON_MD_DELETE_OUTLINE);
delCol, delGlyph);
}
ImGui::PopID();
}
@@ -905,10 +922,16 @@ static void RenderPortfolioEditor(App* app)
else if (delRow == s_pfEdit.sel) s_pfEdit.sel = std::min(delRow, (int)es.size() - 1);
PortfolioBeginEdit(app, s_pfEdit.sel);
}
} else if (clickedSel != -999) {
s_pfConfirmDelIdx = -1; // erase done — storage indices shifted, drop the arm
} else if (clickedSel != -999 && !armedThisFrame) {
pfCommitIfNeeded(app); // auto-save the current group before switching
PortfolioBeginEdit(app, clickedSel);
s_pfConfirmDelIdx = -1; // switching rows disarms any pending delete
}
// A left-click that hit no row/action (empty list space) disarms a pending delete.
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
!ImGui::IsAnyItemHovered())
s_pfConfirmDelIdx = -1;
}
ImGui::EndChild();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
+35 -21
View File
@@ -83,7 +83,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
// over-estimates the grid width and lets the thread cells overflow the card (e.g. at 150%).
float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size * dp, miningBtnMaxW) - miningBtnGap;
float innerW = estControlsW - pad * 2;
float cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f));
float cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size * dp, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f) * dp);
float cellGap = std::max(schema::UI().drawElement("tabs.mining", "cell-gap-min").size, cellSz * schema::UI().drawElement("tabs.mining", "cell-gap-ratio").size);
int cols = std::max(1, std::min(nOpts, (int)(innerW / (cellSz + cellGap))));
int rows = (nOpts + cols - 1) / cols;
@@ -162,14 +162,35 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
const float sy = curY;
ImVec2 savedCur = ImGui::GetCursorScreenPos();
material::IconButtonStyle sideStyle;
sideStyle.color = OnSurfaceMedium();
sideStyle.hoverBg = StateHover();
// -/+ buttons drawn in the same style as the thread tiles (rounded rect + border + centered
// glyph, same fill/border/rounding). Hit-tested with a real InvisibleButton so they stay
// popup-safe.
const float stepRound = schema::UI().drawElement("tabs.mining", "cell-rounding").size;
// atBound: the stepper can't move further (already at 1 for "-" or max for "+"). When
// at the bound the control is a no-op, so render it disabled: dim glyph/border, no hand
// cursor, no tooltip — and swallow the click so it reads as inert.
auto rectStepBtn = [&](const char* id, const char* glyph, float x, const char* tip, bool atBound) -> bool {
ImGui::SetCursorScreenPos(ImVec2(x, sy));
ImGui::InvisibleButton(id, ImVec2(sideW, fieldH));
const bool hov = ImGui::IsItemHovered() && !atBound;
const bool clk = ImGui::IsItemClicked() && !atBound;
const ImVec2 mn(x, sy), mx(x + sideW, sy + fieldH);
dl->AddRectFilled(mn, mx, hov ? WithAlpha(OnSurface(), 25) : WithAlpha(OnSurface(), 8), stepRound);
dl->AddRect(mn, mx, WithAlpha(OnSurface(), atBound ? 15 : (hov ? 80 : 35)), stepRound);
const ImVec2 gsz = stepFont->CalcTextSizeA(stepFont->LegacySize, FLT_MAX, 0, glyph);
dl->AddText(stepFont, stepFont->LegacySize,
ImVec2(x + (sideW - gsz.x) * 0.5f, sy + (fieldH - gsz.y) * 0.5f),
WithAlpha(OnSurface(), atBound ? 30 : (hov ? 160 : 80)), glyph);
if (hov) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (tip && tip[0]) material::Tooltip("%s", tip);
}
return clk;
};
// "-" button
sideStyle.tooltip = TR("mining_threads_minus_tooltip");
ImGui::SetCursorScreenPos(ImVec2(sx, sy));
if (material::IconButton("##ThreadMinus", ICON_MD_REMOVE, stepFont, ImVec2(sideW, fieldH), sideStyle))
// "-" button (disabled at 1 thread)
if (rectStepBtn("##ThreadMinus", ICON_MD_REMOVE, sx, TR("mining_threads_minus_tooltip"),
s_selected_threads <= 1))
applyThreads(s_selected_threads - 1);
sx += sideW + g;
@@ -188,10 +209,9 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
ImGui::PopStyleVar();
sx += fieldW + g;
// "+" button
sideStyle.tooltip = TR("mining_threads_plus_tooltip");
ImGui::SetCursorScreenPos(ImVec2(sx, sy));
if (material::IconButton("##ThreadPlus", ICON_MD_ADD, stepFont, ImVec2(sideW, fieldH), sideStyle))
// "+" button (disabled at max_threads)
if (rectStepBtn("##ThreadPlus", ICON_MD_ADD, sx, TR("mining_threads_plus_tooltip"),
s_selected_threads >= max_threads))
applyThreads(s_selected_threads + 1);
ImGui::SetCursorScreenPos(savedCur);
@@ -738,15 +758,9 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
dl->AddRect(cMin, cMax, WithAlpha(Primary(), (int)(160 + 60 * glow)), rounding, 0, schema::UI().drawElement("tabs.mining", "active-cell-border-thickness").size);
} else if (active) {
// Active but not mining: solid primary fill
ImU32 pri = Primary();
int priR = (pri >> 0) & 0xFF;
int priG = (pri >> 8) & 0xFF;
int priB = (pri >> 16) & 0xFF;
ImU32 fillCol = hovered
? IM_COL32(priR, priG, priB, 220)
: IM_COL32(priR, priG, priB, 180);
ImU32 fillCol = material::WithAlpha(Primary(), hovered ? 220 : 180);
dl->AddRectFilled(cMin, cMax, fillCol, rounding);
dl->AddRect(cMin, cMax, IM_COL32(priR, priG, priB, 255), rounding, 0, schema::UI().drawElement("tabs.mining", "cell-border-thickness").size);
dl->AddRect(cMin, cMax, material::WithAlpha(Primary(), 255), rounding, 0, schema::UI().drawElement("tabs.mining", "cell-border-thickness").size);
} else {
// Inactive: dim outline
ImU32 fillCol = hovered
@@ -938,7 +952,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
else if (poolBlockedBySolo)
material::Tooltip("%s", TR("mining_stop_solo_for_pool"));
else if (poolNeedsPayout)
material::Tooltip("%s", "Enter a payout address first (generate a Z address)");
material::Tooltip("%s", TR("mining_pool_needs_payout_tooltip"));
else
material::Tooltip("%s", isMiningActive ? TR("stop_mining") : TR("start_mining"));
}
+66 -30
View File
@@ -42,6 +42,20 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
bool& s_pool_mode, char (&s_pool_url)[256], char (&s_pool_worker)[256],
bool& s_pool_settings_dirty)
{
// Arm/confirm for the destructive X (delete) on saved pools & workers: a first click on a
// row's X arms it (the X turns red + relabels via the trash glyph) and a second click within
// a short window actually removes it; moving to another row or letting the window lapse disarms.
// Same idiom as contacts_tab.cpp's s_confirm_delete_idx, keyed by row index within each list.
static int s_armed_pool_idx = -1; // armed saved-pool row (-1 = none)
static int s_armed_worker_idx = -1; // armed saved-worker row (-1 = none)
static double s_armed_pool_time = 0.0;
static double s_armed_worker_time = 0.0;
const double kArmWindowSecs = 3.0; // second click must land within this window
const double nowTime = ImGui::GetTime();
// Lapse the arm if the confirm window elapsed.
if (s_armed_pool_idx >= 0 && nowTime - s_armed_pool_time > kArmWindowSecs) s_armed_pool_idx = -1;
if (s_armed_worker_idx >= 0 && nowTime - s_armed_worker_time > kArmWindowSecs) s_armed_worker_idx = -1;
const bool soloMiningAvailable = app->supportsSoloMining();
float toggleW = schema::UI().drawElement("tabs.mining", "mode-toggle-width").size * hs;
float toggleH = schema::UI().drawElement("tabs.mining", "mode-toggle-height").size;
@@ -296,9 +310,11 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
ImFont* rowFont = ImGui::GetFont();
float rowFontSz = ImGui::GetFontSize();
float rowH = ImGui::GetFrameHeight();
int rowIdx = 0;
for (const auto& url : savedUrls) {
ImGui::PushID(url.c_str());
bool isCurrent = (std::string(s_pool_url) == url);
bool armed = (s_armed_pool_idx == rowIdx); // this row's X is arm-confirmed
ImVec2 rowMin = ImGui::GetCursorScreenPos();
ImVec2 rowMax(rowMin.x + popupInnerW, rowMin.y + rowH);
ImGui::InvisibleButton("##row", ImVec2(popupInnerW, rowH));
@@ -319,40 +335,46 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
pdl->AddText(rowFont, rowFontSz,
ImVec2(rowMin.x + textPadX, textY),
isCurrent ? Primary() : OnSurface(), urlDisp.c_str());
// X button — flush with right edge, icon centered
// X (delete) button — flush with right edge, icon centered. First click on the X
// arms it (filled trash glyph + readable-error tint); a second click within the
// arm window actually removes. Armed row stays lit even when not hovered so the
// pending-delete state is visible.
{
ImVec2 xMin(rowMax.x - xZoneW, rowMin.y);
ImVec2 xMax(rowMax.x, rowMax.y);
if (inXZone) {
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("mining_remove"));
} else if (rowHov) {
// Show faint X when row is hovered
ImFont* icoF = Type().iconSmall();
const char* xIcon = ICON_MD_CLOSE;
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
pdl->AddText(icoF, icoF->LegacySize,
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
OnSurfaceDisabled(), xIcon);
material::Tooltip("%s", armed ? TR("address_book_confirm_delete")
: TR("mining_remove"));
}
// Always draw icon when hovering X zone
if (inXZone) {
if (armed || inXZone || rowHov) {
ImFont* icoF = Type().iconSmall();
const char* xIcon = ICON_MD_CLOSE;
// Filled trash while armed reinforces the recolor; otherwise a plain X.
const char* xIcon = armed ? ICON_MD_DELETE : ICON_MD_CLOSE;
ImU32 xCol = armed ? ReadableError()
: (inXZone ? Error() : OnSurfaceDisabled());
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
pdl->AddText(icoF, icoF->LegacySize,
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
Error(), xIcon);
xCol, xIcon);
}
}
// Hovering a *different* row's X disarms the previously armed one.
if (inXZone && !armed && s_armed_pool_idx >= 0)
s_armed_pool_idx = -1;
// Click handling
if (rowClk) {
if (inXZone) {
urlToRemove = url;
if (armed) {
urlToRemove = url; // 2nd click on the armed row removes
} else {
s_armed_pool_idx = rowIdx; // 1st click arms this row
s_armed_pool_time = nowTime;
}
} else {
s_armed_pool_idx = -1; // selecting a row disarms
strncpy(s_pool_url, url.c_str(), sizeof(s_pool_url) - 1);
s_pool_url[sizeof(s_pool_url) - 1] = '\0';
s_pool_settings_dirty = true;
@@ -362,10 +384,12 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
if (rowHov && !inXZone)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::PopID();
rowIdx++;
}
if (!urlToRemove.empty()) {
app->settings()->removeSavedPoolUrl(urlToRemove);
app->settings()->save();
s_armed_pool_idx = -1; // removal shifts indices — drop the arm
}
}
ImGui::EndPopup();
@@ -460,9 +484,11 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
ImFont* wRowFont = ImGui::GetFont();
float wRowFontSz = ImGui::GetFontSize();
float wRowH = ImGui::GetFrameHeight();
int wRowIdx = 0;
for (const auto& addr : savedWorkers) {
ImGui::PushID(addr.c_str());
bool isCurrent = (std::string(s_pool_worker) == addr);
bool armed = (s_armed_worker_idx == wRowIdx); // this row's X is arm-confirmed
ImVec2 rowMin = ImGui::GetCursorScreenPos();
ImVec2 rowMax(rowMin.x + wPopupInnerW, rowMin.y + wRowH);
ImGui::InvisibleButton("##row", ImVec2(wPopupInnerW, wRowH));
@@ -486,38 +512,46 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
// Tooltip for long addresses
if (rowHov && !inXZone)
material::Tooltip("%s", addr.c_str());
// X button — flush with right edge, icon centered
// X (delete) button — flush with right edge, icon centered. First click on the X
// arms it (filled trash glyph + readable-error tint); a second click within the
// arm window actually removes. Armed row stays lit even when not hovered so the
// pending-delete state is visible.
{
ImVec2 xMin(rowMax.x - wXZoneW, rowMin.y);
ImVec2 xMax(rowMax.x, rowMax.y);
if (inXZone) {
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("mining_remove"));
} else if (rowHov) {
ImFont* icoF = Type().iconSmall();
const char* xIcon = ICON_MD_CLOSE;
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
pdl->AddText(icoF, icoF->LegacySize,
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
OnSurfaceDisabled(), xIcon);
material::Tooltip("%s", armed ? TR("address_book_confirm_delete")
: TR("mining_remove"));
}
if (inXZone) {
if (armed || inXZone || rowHov) {
ImFont* icoF = Type().iconSmall();
const char* xIcon = ICON_MD_CLOSE;
// Filled trash while armed reinforces the recolor; otherwise a plain X.
const char* xIcon = armed ? ICON_MD_DELETE : ICON_MD_CLOSE;
ImU32 xCol = armed ? ReadableError()
: (inXZone ? Error() : OnSurfaceDisabled());
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
pdl->AddText(icoF, icoF->LegacySize,
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
Error(), xIcon);
xCol, xIcon);
}
}
// Hovering a *different* row's X disarms the previously armed one.
if (inXZone && !armed && s_armed_worker_idx >= 0)
s_armed_worker_idx = -1;
// Click handling
if (rowClk) {
if (inXZone) {
addrToRemove = addr;
if (armed) {
addrToRemove = addr; // 2nd click on the armed row removes
} else {
s_armed_worker_idx = wRowIdx; // 1st click arms this row
s_armed_worker_time = nowTime;
}
} else {
s_armed_worker_idx = -1; // selecting a row disarms
strncpy(s_pool_worker, addr.c_str(), sizeof(s_pool_worker) - 1);
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
s_pool_settings_dirty = true;
@@ -527,10 +561,12 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
if (rowHov && !inXZone)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::PopID();
wRowIdx++;
}
if (!addrToRemove.empty()) {
app->settings()->removeSavedPoolWorker(addrToRemove);
app->settings()->save();
s_armed_worker_idx = -1; // removal shifts indices — drop the arm
}
}
ImGui::EndPopup();
+50 -13
View File
@@ -665,14 +665,26 @@ void RenderPeersTab(App* app)
dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ping-dot-x-offset").size, cy + body2->LegacySize * 0.5f), pingDotR, dotCol);
float addrX = cx + S.drawElement("tabs.peers", "address-x-offset").size;
// Reserve the line-1 trailing zone (ping + direction pill live on the far right,
// the nearest being the ping at innerW - pingW - spacingXl*3). A long IPv6+port
// addr must clip before it so it can't run under those. Mirror the ping formula
// conservatively and leave a gap.
float line1RightLimit = rowPos.x + innerW - Layout::spacingXl() * 3 -
capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, "0000ms").x -
Layout::spacingSm();
dl->PushClipRect(ImVec2(addrX, rowPos.y), ImVec2(line1RightLimit, rowEnd.y), true);
dl->AddText(body2, body2->LegacySize, ImVec2(addrX, cy), OnSurface(), peer.addr.c_str());
dl->PopClipRect();
// Seed node icon — rendered right after the IP address
// Seed node icon — rendered right after the IP address, but never past the
// reserved trailing zone (a long addr would otherwise push it into the ping text).
if (IsSeedNode(peer.addr)) {
ImVec2 addrSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, peer.addr.c_str());
ImFont* iconFont = Type().iconSmall();
float iconY = cy + (body2->LegacySize - iconFont->LegacySize) * 0.5f;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(addrX + addrSz.x + Layout::spacingSm(), iconY), WithAlpha(Success(), 200), ICON_MD_GRASS);
float seedIconX = std::min(addrX + addrSz.x + Layout::spacingSm(),
line1RightLimit - iconFont->LegacySize);
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(seedIconX, iconY), WithAlpha(Success(), 200), ICON_MD_GRASS);
}
{
@@ -696,30 +708,55 @@ void RenderPeersTab(App* app)
}
float cy2 = cy + body2->LegacySize + Layout::spacingXs();
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size, cy2),
OnSurfaceDisabled(), peer.subver.c_str());
float subverX = cx + S.drawElement("tabs.peers", "address-x-offset").size;
// Reserve the line-2 trailing widths up front so an untrusted, arbitrarily long
// remote subver cannot push the TLS badge / ban-score off the row. Order right→left:
// ban-score pinned far right, then the TLS/no-TLS badge, then the clipped subver text.
float line2RightLimit = rowPos.x + innerW - Layout::spacingLg();
float banScoreLeftX = line2RightLimit;
char banBuf[16];
ImU32 banCol = 0;
bool haveBanScore = peer.banscore > 0;
if (haveBanScore) {
snprintf(banBuf, sizeof(banBuf), TR("peers_ban_score"), peer.banscore);
banCol = peer.banscore > 50 ? Error() : Warning();
float banW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banBuf).x;
banScoreLeftX = line2RightLimit - banW;
// The TLS/no-TLS badge sits to the LEFT of the ban score with a gap.
line2RightLimit = banScoreLeftX - Layout::spacingLg();
}
float tlsBadgeW = std::max(S.drawElement("tabs.peers", "tls-badge-min-width").size, S.drawElement("tabs.peers", "tls-badge-width").size * hs);
float noTlsW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("peers_no_tls")).x;
float badgeW = peer.tls_cipher.empty() ? noTlsW : tlsBadgeW;
// Hard right boundary for the subver text column, leaving room for the badge.
float subverMaxX = line2RightLimit - badgeW - Layout::spacingSm();
if (subverMaxX < subverX) subverMaxX = subverX;
float verW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, peer.subver.c_str()).x;
float tlsBadgeW = std::max(S.drawElement("tabs.peers", "tls-badge-min-width").size, S.drawElement("tabs.peers", "tls-badge-width").size * hs);
dl->PushClipRect(ImVec2(subverX, rowPos.y), ImVec2(subverMaxX, rowEnd.y), true);
dl->AddText(capFont, capFont->LegacySize, ImVec2(subverX, cy2),
OnSurfaceDisabled(), peer.subver.c_str());
dl->PopClipRect();
// Pin the badge just after the (clipped) subver, but never past the reserved zone.
float badgeX = std::min(subverX + verW + Layout::spacingSm(), subverMaxX + Layout::spacingSm());
if (!peer.tls_cipher.empty()) {
ImU32 tlsBg = WithAlpha(Success(), 25);
ImU32 tlsFg = WithAlpha(Success(), 200);
ImVec2 tlsMin(cx + S.drawElement("tabs.peers", "address-x-offset").size + verW + Layout::spacingSm(), cy2);
ImVec2 tlsMin(badgeX, cy2);
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2);
dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size);
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4, cy2 + 1), tlsFg, "TLS");
} else {
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size + verW + Layout::spacingSm(), cy2),
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2),
WithAlpha(Error(), 140), TR("peers_no_tls"));
}
if (peer.banscore > 0) {
char banBuf[16];
snprintf(banBuf, sizeof(banBuf), TR("peers_ban_score"), peer.banscore);
ImU32 banCol = peer.banscore > 50 ? Error() : Warning();
ImVec2 banSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banBuf);
if (haveBanScore) {
dl->AddText(capFont, capFont->LegacySize,
ImVec2(rowPos.x + innerW - banSz.x - Layout::spacingLg(), cy2), banCol, banBuf);
ImVec2(banScoreLeftX, cy2), banCol, banBuf);
}
ImGui::InvisibleButton("##peerRow", ImVec2(innerW, rowH));
+12 -8
View File
@@ -514,21 +514,25 @@ void RenderReceiveTab(App* app)
if (state.addresses.empty()) {
ImVec2 emptyMin = ImGui::GetCursorScreenPos();
float emptyH = S.drawElement("tabs.receive", "skeleton-height").size;
// Hand-drawn skeleton geometry is absolute px — scale heights/offsets/rounding by
// dpiScale() so it doesn't render native-thin at >100% (width ratios multiply formW, leave as-is).
float dp = Layout::dpiScale();
float emptyH = S.drawElement("tabs.receive", "skeleton-height").size * dp;
ImVec2 emptyMax(emptyMin.x + formW, emptyMin.y + emptyH);
DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec);
float alpha = (float)(schema::UI().drawElement("animations", "skeleton-base").size + schema::UI().drawElement("animations", "skeleton-amp").size * std::sin(ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-slow").size));
ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255));
float skelRound = schema::UI().drawElement("tabs.receive", "skeleton-rounding").size * dp;
dl->AddRectFilled(
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg()),
ImVec2(emptyMin.x + formW * S.drawElement("tabs.receive", "skeleton-bar1-width-ratio").size, emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar1-height").size),
skelCol, schema::UI().drawElement("tabs.receive", "skeleton-rounding").size);
ImVec2(emptyMin.x + formW * S.drawElement("tabs.receive", "skeleton-bar1-width-ratio").size, emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar1-height").size * dp),
skelCol, skelRound);
dl->AddRectFilled(
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-top").size),
ImVec2(emptyMin.x + formW * S.drawElement("tabs.receive", "skeleton-bar2-width-ratio").size, emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-bottom").size),
skelCol, schema::UI().drawElement("tabs.receive", "skeleton-rounding").size);
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-top").size * dp),
ImVec2(emptyMin.x + formW * S.drawElement("tabs.receive", "skeleton-bar2-width-ratio").size, emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-bottom").size * dp),
skelCol, skelRound);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - S.drawElement("tabs.receive", "skeleton-text-bottom-offset").size),
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - S.drawElement("tabs.receive", "skeleton-text-bottom-offset").size * dp),
OnSurfaceDisabled(), TR("loading_addresses"));
ImGui::Dummy(ImVec2(formW, emptyH));
ImGui::EndGroup();
@@ -778,7 +782,7 @@ void RenderReceiveTab(App* app)
size_t memo_len = strlen(s_request_memo);
size_t memoMax = (size_t)S.drawElement("business", "memo-max-length").size;
bool memoAtCap = memo_len + 1 >= memoMax;
snprintf(buf, sizeof(buf), "%zu / %zu bytes", memo_len, memoMax);
snprintf(buf, sizeof(buf), TR("byte_count_fmt"), memo_len, memoMax);
Type().textColored(TypeStyle::Caption, memoAtCap ? Warning() : OnSurfaceDisabled(), buf);
}
+10 -8
View File
@@ -424,11 +424,13 @@ static void RenderAmountBar(ImDrawList* dl, double available, float innerW,
? std::clamp((float)((s_amount + s_fee) / available), 0.0f, 1.0f)
: 0.0f;
float maxBtnW = schema::UI().drawElement("tabs.send", "amount-bar-max-btn-width").size;
const float dp = Layout::dpiScale();
float maxBtnW = schema::UI().drawElement("tabs.send", "amount-bar-max-btn-width").size * dp;
float gap = Layout::spacingMd();
float barW = innerW - maxBtnW - gap;
if (barW < schema::UI().drawElement("tabs.send", "progress-bar-min-width").size) barW = schema::UI().drawElement("tabs.send", "progress-bar-min-width").size;
float barH = schema::UI().drawElement("tabs.send", "amount-bar-height").size;
float minBarW = schema::UI().drawElement("tabs.send", "progress-bar-min-width").size * dp;
if (barW < minBarW) barW = minBarW;
float barH = schema::UI().drawElement("tabs.send", "amount-bar-height").size * dp;
float barRound = barH * 0.5f;
ImVec2 barMin = ImGui::GetCursorScreenPos();
@@ -1000,7 +1002,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
ImGui::BeginDisabled(!can_send);
char sendId[64];
snprintf(sendId, sizeof(sendId), "Review Send%s", suffix);
snprintf(sendId, sizeof(sendId), "%s##ReviewSend%s", TR("review_send"), suffix);
if (TactileButton(sendId, ImVec2(sendBtnW, btnH), S.resolveFont(S.button("tabs.send", "send-button").font))) {
s_show_confirm = true;
}
@@ -1020,7 +1022,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
else if (total > available)
material::Tooltip("%s", TR("send_tooltip_exceeds_balance"));
else if (!sourceSpendable)
material::Tooltip("%s", "View-only address — no spending key, cannot send");
material::Tooltip("%s", TR("send_tooltip_view_only"));
else if (s_sending)
material::Tooltip("%s", TR("send_tooltip_in_progress"));
}
@@ -1033,7 +1035,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
ImGui::PushStyleColor(ImGuiCol_Border, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, S.drawElement("tabs.send", "cancel-btn-border-size").size);
char clearId[64];
snprintf(clearId, sizeof(clearId), "Cancel%s", suffix);
snprintf(clearId, sizeof(clearId), "%s##Cancel%s", TR("cancel"), suffix);
if (TactileButton(clearId, ImVec2(cancelBtnW, btnH), S.resolveFont(S.button("tabs.send", "clear-button").font))) {
if (FormHasData()) {
s_clear_confirm_pending = true;
@@ -1377,7 +1379,7 @@ void RenderSendTab(App* app)
s_preview_text.c_str(), s_preview_text.c_str() + std::min(s_preview_text.size(), (size_t)S.drawElement("tabs.send", "paste-preview-max-chars").size));
}
if (TactileButton("Paste##to", ImVec2(pasteW, 0), S.resolveFont(S.button("tabs.send", "paste-button").font))) {
if (TactileButton((std::string(TR("paste")) + "##to").c_str(), ImVec2(pasteW, 0), S.resolveFont(S.button("tabs.send", "paste-button").font))) {
if (s_paste_previewing) {
// Commit the preview
snprintf(s_to_address, sizeof(s_to_address), "%s", s_preview_text.c_str());
@@ -1547,7 +1549,7 @@ void RenderSendTab(App* app)
size_t memo_len = strlen(s_memo);
size_t memoMax = (size_t)S.drawElement("business", "memo-max-length").size;
bool memoAtCap = memo_len + 1 >= memoMax;
snprintf(buf, sizeof(buf), "%zu / %zu bytes", memo_len, memoMax);
snprintf(buf, sizeof(buf), TR("byte_count_fmt"), memo_len, memoMax);
Type().textColored(TypeStyle::Caption, memoAtCap ? Warning() : OnSurfaceDisabled(), buf);
}