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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user