feat(settings): shared design components + Wallet button hierarchy

Step 1: promote the polished chat-settings building blocks into reusable material
components (src/ui/material/settings_controls.h) — SettingsSubheader (accent
small-caps), SettingsRow (label left / control right), SegmentedControl (iOS
track+pill), and a tiered ActionButton (Primary=accent fill, Secondary=glass,
Tertiary=ghost, Destructive=error) with an optional leading Material icon, plus a
ButtonFlow that wraps rows of buttons.

Step 2: rebuild the Wallet BACKUP & DATA button wall on them. The 9–11 identical
buttons that were font-scaled onto one row are now tier-ordered, icon-labelled,
and wrap to new rows: the emphasized actions the user flagged — Import key, Seed
phrase, Wallets…, Download Bootstrap — cluster on top as accent Primary buttons;
common actions (viewing-key import, Backup, Migrate, Setup Wizard) are Secondary;
exports are low-emphasis Tertiary. All click handlers/tooltips and the migrate
"legacy wallet" glow are preserved. Removes the SetWindowFontScale legibility hack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 15:48:21 -05:00
parent 203967411a
commit 95ff9ce9ae
2 changed files with 239 additions and 92 deletions

View File

@@ -31,6 +31,7 @@
#include "../effects/low_spec.h"
#include "../effects/scroll_fade_shader.h"
#include "../material/draw_helpers.h"
#include "../material/settings_controls.h"
#include "../material/type.h"
#include "../material/colors.h"
#include "../windows/validate_address_dialog.h"
@@ -1390,119 +1391,72 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_clear_ztx"));
}
// --- Backup & Data (merged in from its former standalone card) ---
// --- Backup & Data ---
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("backup_data"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
{
float btnPad = S.drawElement("components.settings-page", "wallet-btn-padding").sizeOr(24.0f);
const char* r1[] = {TR("settings_import_key"), TR("settings_export_key"), TR("settings_export_all"), TR("settings_backup"), TR("settings_export_csv")};
const char* t1[] = { TR("tt_import_key"), TR("tt_export_key"), TR("tt_export_all"), TR("tt_backup"), TR("tt_export_csv") };
const bool showFullNodeLifecycleActions = app->supportsFullNodeLifecycleActions();
const char* wizLabel = TR("setup_wizard");
const char* bsLabel = TR("download_bootstrap");
float sp = Layout::spacingSm();
ImFont* btnFont = S.resolveFont("button");
using AT = material::ActionTier;
const bool fullNode = app->supportsFullNodeLifecycleActions();
// Tier-ordered, icon-labelled buttons that WRAP to new rows (no more font-scale-to-fit).
// Emphasized actions (accent) cluster on top, then common actions, then low-emphasis exports.
auto btn = [&](material::ButtonFlow& fl, const char* id, const char* label, const char* icon,
AT tier, const char* tip) -> bool {
fl.next(material::ActionButtonWidth(label, icon));
const bool p = material::ActionButton(id, label, icon, tier);
if (ImGui::IsItemHovered() && tip && tip[0]) material::Tooltip("%s", tip);
return p;
};
float btnPadX = btnPad * 2;
float naturalW = 0;
for (int i = 0; i < 5; i++)
naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX;
float impViewW = ImGui::CalcTextSize(TR("settings_import_viewkey")).x + btnPadX;
naturalW += impViewW; // the extra "Import viewing key" button (rendered after Import key)
float wizW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(wizLabel).x + btnPadX : 0.0f;
float bsW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(bsLabel).x + btnPadX : 0.0f;
// Full-node-only "Seed phrase" + "Migrate to seed" buttons trail the Backup button.
float seedW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(TR("seed_backup_button")).x + btnPadX : 0.0f;
float migrateW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(TR("seed_migrate_button")).x + btnPadX : 0.0f;
float totalW = naturalW + sp * 6; // 6 base buttons (incl. Import viewing key)
if (showFullNodeLifecycleActions) totalW += wizW + bsW + seedW + migrateW + sp * 4;
float scale = (totalW > contentW) ? contentW / totalW : 1.0f;
if (scale < 1.0f) ImGui::SetWindowFontScale(scale);
float scaledSp = sp * scale;
if (TactileButton(r1[0], ImVec2(0, 0), btnFont))
// Emphasized (Primary): import key + (full-node) seed / wallets / bootstrap.
material::ButtonFlow fPrim(contentW);
if (btn(fPrim, "##imp_key", TR("settings_import_key"), ICON_MD_KEY, AT::Primary, TR("tt_import_key")))
app->showImportKeyDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[0]);
// Watch-only shielded viewing-key import (separate button — different key type + scan-height).
ImGui::SameLine(0, scaledSp);
if (TactileButton(TR("settings_import_viewkey"), ImVec2(0, 0), btnFont))
app->showImportViewingKeyDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_import_viewkey"));
ImGui::SameLine(0, scaledSp);
if (TactileButton(r1[1], ImVec2(0, 0), btnFont))
app->showExportKeyDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[1]);
ImGui::SameLine(0, scaledSp);
if (TactileButton(r1[2], ImVec2(0, 0), btnFont))
ExportAllKeysDialog::show();
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[2]);
ImGui::SameLine(0, scaledSp);
if (TactileButton(r1[3], ImVec2(0, 0), btnFont))
app->showBackupDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[3]);
if (showFullNodeLifecycleActions) {
// Back up the wallet's BIP39 seed phrase (full-node mnemonic wallets).
ImGui::SameLine(0, scaledSp);
if (TactileButton(TR("seed_backup_button"), ImVec2(0, 0), btnFont))
if (fullNode) {
if (btn(fPrim, "##seed", TR("seed_backup_button"), ICON_MD_VPN_KEY, AT::Primary, TR("tt_seed_backup")))
app->showSeedBackupDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_backup"));
// Migrate to a new seed-phrase wallet (create in isolation, then sweep funds). A
// legacy (pre-seed-phrase) wallet glows the button to nudge the user toward migrating.
ImGui::SameLine(0, scaledSp);
if (btn(fPrim, "##wallets", TR("wallets_button"), ICON_MD_ACCOUNT_BALANCE_WALLET, AT::Primary, TR("tt_wallets_button")))
ui::WalletsDialog::show(app);
if (btn(fPrim, "##bootstrap", TR("download_bootstrap"), ICON_MD_CLOUD_DOWNLOAD, AT::Primary, TR("tt_download_bootstrap")))
BootstrapDownloadDialog::show(app);
}
// Common (Secondary): viewing-key import, backup, (full-node) migrate + setup wizard.
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
material::ButtonFlow fSec(contentW);
if (btn(fSec, "##imp_vk", TR("settings_import_viewkey"), ICON_MD_VISIBILITY, AT::Secondary, TR("tt_import_viewkey")))
app->showImportViewingKeyDialog();
if (btn(fSec, "##backup", TR("settings_backup"), ICON_MD_BACKUP, AT::Secondary, TR("tt_backup")))
app->showBackupDialog();
if (fullNode) {
const bool migrateGlow = app->isPreSeedWallet();
if (TactileButton(TR("seed_migrate_button"), ImVec2(0, 0), btnFont))
if (btn(fSec, "##migrate", TR("seed_migrate_button"), ICON_MD_SWAP_HORIZ, AT::Secondary, TR("tt_seed_migrate")))
app->showSeedMigrationDialog();
if (migrateGlow) {
if (migrateGlow) { // pulsing accent halo nudging a legacy wallet to migrate
const ImVec2 gmn = ImGui::GetItemRectMin(), gmx = ImGui::GetItemRectMax();
const float gdp = Layout::dpiScale();
const float pulse = 0.5f + 0.5f * std::sin((float)ImGui::GetTime() * 3.2f); // 0..1
const float pulse = 0.5f + 0.5f * std::sin((float)ImGui::GetTime() * 3.2f);
ImDrawList* gdl = ImGui::GetWindowDrawList();
for (int g = 3; g >= 1; --g) { // soft, pulsing outward halo in the accent color
for (int g = 3; g >= 1; --g) {
const float e = (float)g * 2.2f * gdp;
const int a = (int)((70.0f + pulse * 95.0f) / (float)g);
gdl->AddRect(ImVec2(gmn.x - e, gmn.y - e), ImVec2(gmx.x + e, gmx.y + e),
material::WithAlpha(material::Primary(), a), 6.0f * gdp + e, 0, 1.6f * gdp);
}
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_migrate"));
// Multi-wallet: list wallet files + switch the active one.
ImGui::SameLine(0, scaledSp);
if (TactileButton(TR("wallets_button"), ImVec2(0, 0), btnFont))
ui::WalletsDialog::show(app);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_wallets_button"));
}
ImGui::SameLine(0, scaledSp);
if (TactileButton(r1[4], ImVec2(0, 0), btnFont))
ExportTransactionsDialog::show();
if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[4]);
if (showFullNodeLifecycleActions) {
// Right-align Setup Wizard + Download Bootstrap.
float framePadX2 = ImGui::GetStyle().FramePadding.x * 2.0f;
float curX = ImGui::GetCursorScreenPos().x;
float wizBtnW = ImGui::CalcTextSize(wizLabel).x + framePadX2;
float bsBtnW = ImGui::CalcTextSize(bsLabel).x + framePadX2;
float rightEdge = cardMin.x + availWidth - pad;
float rightGroupW = bsBtnW + scaledSp + wizBtnW;
float groupX = rightEdge - rightGroupW;
if (groupX > curX) {
ImGui::SameLine(0, 0);
ImGui::SetCursorScreenPos(ImVec2(groupX, ImGui::GetCursorScreenPos().y));
} else {
ImGui::SameLine(0, scaledSp);
}
if (TactileButton(bsLabel, ImVec2(0, 0), btnFont))
BootstrapDownloadDialog::show(app);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_download_bootstrap"));
ImGui::SameLine(0, scaledSp);
if (TactileButton(wizLabel, ImVec2(0, 0), btnFont))
if (btn(fSec, "##wizard", TR("setup_wizard"), ICON_MD_AUTO_FIX_HIGH, AT::Secondary, TR("tt_wizard")))
app->restartWizard();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_wizard"));
}
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
// Low-emphasis (Tertiary): exports.
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
material::ButtonFlow fTer(contentW);
if (btn(fTer, "##exp_key", TR("settings_export_key"), ICON_MD_LOGOUT, AT::Tertiary, TR("tt_export_key")))
app->showExportKeyDialog();
if (btn(fTer, "##exp_all", TR("settings_export_all"), ICON_MD_ARCHIVE, AT::Tertiary, TR("tt_export_all")))
ExportAllKeysDialog::show();
if (btn(fTer, "##exp_csv", TR("settings_export_csv"), ICON_MD_DESCRIPTION, AT::Tertiary, TR("tt_export_csv")))
ExportTransactionsDialog::show();
}
}