diff --git a/res/lang/de.json b/res/lang/de.json index 91bdd68..8f9f4fa 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -1312,7 +1312,7 @@ "sent_filter": "Gesendet", "sent_type": "Gesendet", "sent_upper": "GESENDET", - "set_label": "Label setzen...", + "set_label": "Label setzen", "settings": "Einstellungen", "settings_about_text": "Eine geschirmte Kryptowährungs-Wallet für DragonX (DRGX), erstellt mit Dear ImGui für ein leichtes, portables Erlebnis.", "settings_acrylic_level": "Acrylstufe:", diff --git a/res/lang/es.json b/res/lang/es.json index 3a948d0..f9ef36f 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -1312,7 +1312,7 @@ "sent_filter": "Enviado", "sent_type": "Enviado", "sent_upper": "ENVIADO", - "set_label": "Establecer Etiqueta...", + "set_label": "Establecer Etiqueta", "settings": "Ajustes", "settings_about_text": "Una billetera de criptomonedas blindada para DragonX (DRGX), creada con Dear ImGui para una experiencia ligera y portátil.", "settings_acrylic_level": "Nivel de acrílico:", diff --git a/res/lang/fr.json b/res/lang/fr.json index ee412c0..2dbf5b3 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -1312,7 +1312,7 @@ "sent_filter": "Envoyé", "sent_type": "Envoyé", "sent_upper": "ENVOYÉ", - "set_label": "Définir le libellé...", + "set_label": "Définir le libellé", "settings": "Paramètres", "settings_about_text": "Un portefeuille de cryptomonnaie blindé pour DragonX (DRGX), construit avec Dear ImGui pour une expérience légère et portable.", "settings_acrylic_level": "Niveau acrylique :", diff --git a/res/lang/ja.json b/res/lang/ja.json index 24396ce..d5c42c3 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -1309,7 +1309,7 @@ "sent_filter": "送信済み", "sent_type": "送信済み", "sent_upper": "送信済み", - "set_label": "ラベルを設定...", + "set_label": "ラベルを設定", "settings": "設定", "settings_about_text": "DragonX (DRGX) 用のシールド暗号通貨ウォレット。Dear ImGui で構築された軽量でポータブルな体験。", "settings_acrylic_level": "アクリルレベル:", diff --git a/res/lang/ko.json b/res/lang/ko.json index 4c5c018..a015d8d 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -1311,7 +1311,7 @@ "sent_filter": "전송됨", "sent_type": "전송됨", "sent_upper": "전송됨", - "set_label": "라벨 설정...", + "set_label": "라벨 설정", "settings": "설정", "settings_about_text": "DragonX (DRGX)용 차폐 암호화폐 지갑으로, Dear ImGui로 제작되어 가볍고 휴대 가능합니다.", "settings_acrylic_level": "아크릴 레벨:", diff --git a/res/lang/pt.json b/res/lang/pt.json index f0c8b62..97a95e5 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -1312,7 +1312,7 @@ "sent_filter": "Enviado", "sent_type": "Enviado", "sent_upper": "ENVIADO", - "set_label": "Definir Rótulo...", + "set_label": "Definir Rótulo", "settings": "Ajustes", "settings_about_text": "Uma carteira de criptomoeda blindada para DragonX (DRGX), criada com Dear ImGui para uma experiência leve e portátil.", "settings_acrylic_level": "Nível acrílico:", diff --git a/res/lang/ru.json b/res/lang/ru.json index 8622246..deef9a3 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -1312,7 +1312,7 @@ "sent_filter": "Отправлено", "sent_type": "Отправлено", "sent_upper": "ОТПРАВЛЕНО", - "set_label": "Установить метку...", + "set_label": "Установить метку", "settings": "Настройки", "settings_about_text": "Защищённый криптовалютный кошелёк для DragonX (DRGX), созданный на Dear ImGui для лёгкого и портативного использования.", "settings_acrylic_level": "Уровень акрила:", diff --git a/res/lang/zh.json b/res/lang/zh.json index 1e2691a..461352f 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -1310,7 +1310,7 @@ "sent_filter": "已发送", "sent_type": "已发送", "sent_upper": "已发送", - "set_label": "设置标签...", + "set_label": "设置标签", "settings": "设置", "settings_about_text": "DragonX (DRGX) 屏蔽加密货币钱包,使用 Dear ImGui 构建,提供轻量、便携的体验。", "settings_acrylic_level": "亚克力级别:", diff --git a/src/app_security.cpp b/src/app_security.cpp index 838bba2..36be4c7 100644 --- a/src/app_security.cpp +++ b/src/app_security.cpp @@ -1476,12 +1476,22 @@ void App::renderEncryptWalletDialog() { bool valid = strlen(change_old_pass_buf_) > 0 && strlen(change_new_pass_buf_) >= 8 && strcmp(change_new_pass_buf_, change_confirm_buf_) == 0; + + // Two-button footer (primary + Cancel) to match the encrypt/decrypt siblings. + float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; ImGui::BeginDisabled(!valid || encrypt_in_progress_); - if (ui::material::TactileButton(TR("change_pass_title"), ImVec2(-1, 40))) { + if (ui::material::TactileButton(TR("change_pass_title"), ImVec2(btnW, 40))) { changePassphrase(std::string(change_old_pass_buf_), std::string(change_new_pass_buf_)); } ImGui::EndDisabled(); + + ImGui::SameLine(); + // Cancel does what Esc/close does — dismiss without applying. Buffers are wiped by + // the !show_change_passphrase_ cleanup block below. + if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) { + show_change_passphrase_ = false; + } EndOverlayDialog(); } @@ -1945,8 +1955,10 @@ void App::renderPinDialogs() { util::SecureVault::isValidPin(pinStr) && strcmp(pin_buf_, pin_confirm_buf_) == 0; + // Two-button footer (primary + Cancel) to match the encrypt/decrypt siblings. + float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; ImGui::BeginDisabled(!valid || pin_in_progress_); - if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(-1, 40))) { + if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) { pin_in_progress_ = true; pin_status_ = "Verifying passphrase..."; @@ -2005,6 +2017,13 @@ void App::renderPinDialogs() { } } ImGui::EndDisabled(); + + ImGui::SameLine(); + // Cancel does what Esc/close does — dismiss without applying. Buffers are wiped by + // the !show_pin_setup_ cleanup block below. + if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) { + show_pin_setup_ = false; + } EndOverlayDialog(); } // Wipe the passphrase/PIN buffers if the dialog was dismissed (X / Esc / diff --git a/src/ui/material/draw_helpers.h b/src/ui/material/draw_helpers.h index 8212376..697729f 100644 --- a/src/ui/material/draw_helpers.h +++ b/src/ui/material/draw_helpers.h @@ -79,6 +79,63 @@ inline const char* LoadingDots() { return kDots[n]; } +// ── Centered empty state ───────────────────────────────────────────────── +// A big muted icon + title + optional wrapped hint, centered on BOTH axes within +// GetContentRegionAvail(). Mirrors chat_tab's centeredEmptyState so list-empty states +// read the same across tabs. Call at the start of the region you want it centered in +// (e.g. right after a BeginChild / a leading Dummy). Font metrics use the live font +// scale (LegacySize * FontScaleMain) and PushFont draws at that same scale, so this is +// crisp at HiDPI / font_scale 1.5 without any manual dpiScale multiply on the metrics. +inline void DrawEmptyState(const char* iconGlyph, const char* title, const char* hint = nullptr) +{ + auto scaled = [](ImFont* f) { return f->LegacySize * ImGui::GetStyle().FontScaleMain; }; + const ImVec2 avail = ImGui::GetContentRegionAvail(); + const ImVec2 origin = ImGui::GetCursorPos(); + ImFont* iconF = Type().iconXL(); + ImFont* titleF = Type().subtitle1(); + ImFont* hintF = Type().body2(); + const float dp = Layout::dpiScale(); + const float gap = 8.0f * dp; + const float wrap = std::min(avail.x - 40.0f * dp, 360.0f * dp); + const float iconSz = iconF ? scaled(iconF) : 40.0f; + const float iconH = (iconF && iconGlyph) ? iconF->CalcTextSizeA(iconSz, FLT_MAX, 0.0f, iconGlyph).y : 0.0f; + const float titleH = titleF->CalcTextSizeA(scaled(titleF), FLT_MAX, 0.0f, title).y; + const float hintH = hint ? hintF->CalcTextSizeA(scaled(hintF), wrap, wrap, hint).y : 0.0f; + const float totalH = iconH + (iconH > 0.0f ? gap : 0.0f) + titleH + (hint ? gap + hintH : 0.0f); + float y = origin.y + std::max(0.0f, (avail.y - totalH) * 0.5f); + + if (iconF && iconGlyph && iconGlyph[0]) { + const float iw = iconF->CalcTextSizeA(iconSz, FLT_MAX, 0.0f, iconGlyph).x; + ImGui::SetCursorPos(ImVec2(origin.x + (avail.x - iw) * 0.5f, y)); + ImGui::PushFont(iconF); + ImGui::PushStyleColor(ImGuiCol_Text, WithAlpha(OnSurface(), 70)); + ImGui::TextUnformatted(iconGlyph); + ImGui::PopStyleColor(); + ImGui::PopFont(); + y += iconH + gap; + } + { + const float tw = titleF->CalcTextSizeA(scaled(titleF), FLT_MAX, 0.0f, title).x; + ImGui::SetCursorPos(ImVec2(origin.x + (avail.x - tw) * 0.5f, y)); + ImGui::PushFont(titleF); + ImGui::PushStyleColor(ImGuiCol_Text, OnSurfaceMedium()); + ImGui::TextUnformatted(title); + ImGui::PopStyleColor(); + ImGui::PopFont(); + y += titleH + gap; + } + if (hint) { + ImGui::SetCursorPos(ImVec2(origin.x + (avail.x - wrap) * 0.5f, y)); + ImGui::PushFont(hintF); + ImGui::PushStyleColor(ImGuiCol_Text, WithAlpha(OnSurface(), 120)); + ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + wrap); + ImGui::TextUnformatted(hint); + ImGui::PopTextWrapPos(); + ImGui::PopStyleColor(); + ImGui::PopFont(); + } +} + // ============================================================================ // Text Drop Shadow // ============================================================================ diff --git a/src/ui/windows/about_dialog.cpp b/src/ui/windows/about_dialog.cpp index d7c5205..f58ce5c 100644 --- a/src/ui/windows/about_dialog.cpp +++ b/src/ui/windows/about_dialog.cpp @@ -125,29 +125,26 @@ void RenderAboutDialog(App* app, bool* p_open) ImGui::Spacing(); ImGui::TextWrapped("%s", TR("about_license_text")); - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - // Links - if (material::StyledButton(TR("about_website"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) { + // Links — 3-button action row, centered via the shared footer helper (draws its own + // Spacing/Separator/Spacing above the row, replacing the hand-rolled divider block). + const float linksTotalW = linkW * 3.0f + ImGui::GetStyle().ItemSpacing.x * 2.0f; + material::BeginOverlayDialogFooter(linksTotalW); + if (material::TactileButton(TR("about_website"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) { util::Platform::openUrl("https://dragonx.is"); } ImGui::SameLine(); - if (material::StyledButton(TR("about_github"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) { + if (material::TactileButton(TR("about_github"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) { util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon"); } ImGui::SameLine(); - if (material::StyledButton(TR("about_block_explorer"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) { + if (material::TactileButton(TR("about_block_explorer"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) { util::Platform::openUrl("https://explorer.dragonx.is"); } - - ImGui::Spacing(); - - // Close button - float button_width = closeW; - ImGui::SetCursorPosX((ImGui::GetWindowWidth() - button_width) * 0.5f); - if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) { + + // Close button — lone dismiss action, centered via the shared footer helper (no extra + // divider above it, so it sits directly under the links row). + material::BeginOverlayDialogFooter(closeW, false); + if (material::TactileButton(TR("close"), ImVec2(closeW, 0), S.resolveFont(closeBtn.font))) { *p_open = false; } diff --git a/src/ui/windows/address_transfer_dialog.h b/src/ui/windows/address_transfer_dialog.h index 5c78d2a..c6baad1 100644 --- a/src/ui/windows/address_transfer_dialog.h +++ b/src/ui/windows/address_transfer_dialog.h @@ -180,16 +180,14 @@ public: const char* sendingLabel = TR("sending"); ImFont* buttonFont = Type().button(); float buttonFontSize = ScaledFontSize(buttonFont); - float minBtnW = 120.0f * dp; float confirmMinW = 160.0f * dp; float buttonPadW = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp; - float cancelW = std::max(minBtnW, - buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, cancelLabel).x + buttonPadW); + // Both footer buttons share one width (equal-width primary/Close pair), sized to fit the + // widest label — the "Sending…" swap label included — so nothing clips. float confirmTextW = std::max( buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, confirmLabel).x, buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, sendingLabel).x); - float confirmW = std::max(confirmMinW, confirmTextW + buttonPadW); - float totalW = cancelW + confirmW + Layout::spacingMd(); + float btnW = std::max(confirmMinW, confirmTextW + buttonPadW); float footerH = ImGui::GetFrameHeight() + ImGui::GetStyle().ItemSpacing.y * 3.0f; // footer divider removed ImGuiViewport* vp = ImGui::GetMainViewport(); float cardBottomY = vp->Pos.y + vp->Size.y * 0.85f; @@ -201,19 +199,18 @@ public: ImGui::Spacing(); } - ImGui::Spacing(); + // Standardized primary + Close footer (centered, no divider). The primary is the + // Confirm/"Sending…" action; the Close button dismisses the dialog. + bool outConfirm = false; + bool outClose = false; + DialogActionFooter(s_sending ? sendingLabel : confirmLabel, + amountValid && !s_sending, + cancelLabel, outConfirm, outClose, btnW); - float rowStartX = ImGui::GetCursorPosX(); - float contentW = ImGui::GetContentRegionAvail().x; - ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalW) * 0.5f)); - - if (TactileButton(cancelLabel, ImVec2(cancelW, 0), buttonFont)) { + if (outClose) { s_open = false; } - ImGui::SameLine(0, Layout::spacingMd()); - - ImGui::BeginDisabled(!amountValid || s_sending); - if (TactileButton(s_sending ? sendingLabel : confirmLabel, ImVec2(confirmW, 0), buttonFont)) { + if (outConfirm) { s_sending = true; s_app->sendTransaction(s_info.fromAddr, s_info.toAddr, amount, s_fee, "", @@ -231,7 +228,6 @@ public: // state, and when the async callback sets s_resultMsg the in-dialog result screen shows // (with its own Close button). Previously closing here made that result screen dead code. } - ImGui::EndDisabled(); EndOverlayDialog(); } diff --git a/src/ui/windows/block_info_dialog.cpp b/src/ui/windows/block_info_dialog.cpp index 6f266c8..7ce979c 100644 --- a/src/ui/windows/block_info_dialog.cpp +++ b/src/ui/windows/block_info_dialog.cpp @@ -135,7 +135,7 @@ void BlockInfoDialog::render(App* app) ImGui::BeginDisabled(); } - if (material::StyledButton(TR("block_get_info"), ImVec2(0,0), S.resolveFont(closeBtn.font))) { + if (material::TactileButton(TR("block_get_info"), ImVec2(0,0), S.resolveFont(closeBtn.font))) { if (rpc && rpc->isConnected() && app->worker()) { s_loading = true; s_error.clear(); @@ -303,7 +303,7 @@ void BlockInfoDialog::render(App* app) // Navigation buttons if (s_has_data) { if (s_height > 1) { - if (material::StyledButton(TR("block_nav_prev"), ImVec2(0,0), S.resolveFont(closeBtn.font))) { + if (material::TactileButton(TR("block_nav_prev"), ImVec2(0,0), S.resolveFont(closeBtn.font))) { s_height--; s_has_data = false; s_error.clear(); @@ -315,7 +315,7 @@ void BlockInfoDialog::render(App* app) // nextblockhash, so this stays hidden there). if (!s_next_hash.empty() && (state.sync.blocks <= 0 || s_height < state.sync.blocks)) { - if (material::StyledButton(TR("block_nav_next"), ImVec2(0,0), S.resolveFont(closeBtn.font))) { + if (material::TactileButton(TR("block_nav_next"), ImVec2(0,0), S.resolveFont(closeBtn.font))) { s_height++; s_has_data = false; s_error.clear(); @@ -323,9 +323,10 @@ void BlockInfoDialog::render(App* app) } } - // Close button at bottom - ImGui::SetCursorPosY(ImGui::GetWindowHeight() - 40); - if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) { + // Close button at bottom — centered via the shared footer helper (no separator, matching + // the prior hand-rolled placement). + material::BeginOverlayDialogFooter(closeBtn.width, /*drawSeparator=*/false); + if (material::TactileButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) { s_open = false; } material::EndOverlayDialog(); diff --git a/src/ui/windows/chat_tab.cpp b/src/ui/windows/chat_tab.cpp index 191dc59..6dbbb16 100644 --- a/src/ui/windows/chat_tab.cpp +++ b/src/ui/windows/chat_tab.cpp @@ -451,7 +451,7 @@ void RenderChatSettingsPreview(App* app, float width) { totalH += padIn; const ImVec2 origin = ImGui::GetCursorScreenPos(); - material::GlassPanelSpec g; g.rounding = 10.0f * dp; g.fillAlpha = 16; g.borderAlpha = 36; + material::GlassPanelSpec g; g.rounding = Layout::glassRounding(); g.fillAlpha = 16; g.borderAlpha = 36; material::DrawGlassPanel(dl, origin, ImVec2(origin.x + width, origin.y + totalH), g); const float leftX = origin.x + padIn; @@ -795,7 +795,7 @@ void RenderChatTab(App* app) { ImDrawList* paneDL = ImGui::GetWindowDrawList(); const ImVec2 pMin = ImGui::GetCursorScreenPos(); - material::GlassPanelSpec g; g.rounding = 12.0f * Layout::dpiScale(); g.fillAlpha = 20; g.borderAlpha = 34; + material::GlassPanelSpec g; g.rounding = Layout::glassRounding(); g.fillAlpha = 20; g.borderAlpha = 34; material::DrawGlassPanel(paneDL, pMin, ImVec2(pMin.x + listW, pMin.y + avail.y), g); } // Inner padding so the list content (buttons, search, conversation cards) doesn't hug the glass @@ -973,7 +973,7 @@ void RenderChatTab(App* app) ImDrawList* paneDL = ImGui::GetWindowDrawList(); const ImVec2 pMin = ImGui::GetCursorScreenPos(); const float pW = ImGui::GetContentRegionAvail().x; - material::GlassPanelSpec g; g.rounding = 12.0f * tdp; g.fillAlpha = 12; g.borderAlpha = 34; + material::GlassPanelSpec g; g.rounding = Layout::glassRounding(); g.fillAlpha = 12; g.borderAlpha = 34; material::DrawGlassPanel(paneDL, pMin, ImVec2(pMin.x + pW, pMin.y + (avail.y - composerAreaH)), g); } // Inner padding so the header + messages don't hug the glass card's edges (the message child @@ -1622,7 +1622,7 @@ void RenderChatTab(App* app) // FrameBg); a flat FrameBg showed the sharp texture. { ImDrawList* cdl = ImGui::GetWindowDrawList(); - material::GlassPanelSpec g; g.rounding = 8.0f * tdp; g.fillAlpha = 16; g.borderAlpha = 34; + material::GlassPanelSpec g; g.rounding = Layout::glassRounding(); g.fillAlpha = 16; g.borderAlpha = 34; material::DrawGlassPanel(cdl, ImVec2(inputX, rowY), ImVec2(inputX + inputW, rowY + composerBoxH), g); } diff --git a/src/ui/windows/contacts_tab.cpp b/src/ui/windows/contacts_tab.cpp index ff61a36..79f2206 100644 --- a/src/ui/windows/contacts_tab.cpp +++ b/src/ui/windows/contacts_tab.cpp @@ -1026,7 +1026,7 @@ void RenderContactsTab(App* app) const float tpW = ImGui::GetContentRegionAvail().x; const float tIpad = 12.0f * dp, tVpad = 10.0f * dp; { - material::GlassPanelSpec g; g.rounding = 12.0f * dp; g.fillAlpha = 14; g.borderAlpha = 34; + material::GlassPanelSpec g; g.rounding = Layout::glassRounding(); g.fillAlpha = 14; g.borderAlpha = 34; material::DrawGlassPanel(tpdl, tpMin, ImVec2(tpMin.x + tpW, tpMin.y + listH), g); } ImGui::SetCursorScreenPos(ImVec2(tpMin.x + tIpad, tpMin.y + tVpad)); @@ -1129,7 +1129,7 @@ void RenderContactsTab(App* app) ImDrawList* pdl = ImGui::GetWindowDrawList(); const ImVec2 pMin = ImGui::GetCursorScreenPos(); const float pW = ImGui::GetContentRegionAvail().x; - material::GlassPanelSpec g; g.rounding = 12.0f * dp; g.fillAlpha = 14; g.borderAlpha = 34; + material::GlassPanelSpec g; g.rounding = Layout::glassRounding(); g.fillAlpha = 14; g.borderAlpha = 34; material::DrawGlassPanel(pdl, pMin, ImVec2(pMin.x + pW, pMin.y + listH), g); } // AlwaysUseWindowPadding: a borderless child ignores WindowPadding without it, so the rows @@ -1390,7 +1390,7 @@ void RenderContactsTab(App* app) const float rowH = avR * 2.0f + 16.0f * dp; const float panelH = 2.0f * rowH + gap + 2.0f * padIn; const ImVec2 pOrigin = ImGui::GetCursorScreenPos(); - material::GlassPanelSpec g; g.rounding = 12.0f * dp; g.fillAlpha = 14; g.borderAlpha = 34; + material::GlassPanelSpec g; g.rounding = Layout::glassRounding(); g.fillAlpha = 14; g.borderAlpha = 34; material::DrawGlassPanel(pdl, pOrigin, ImVec2(pOrigin.x + w, pOrigin.y + panelH), g); const ImVec2 rmn(pOrigin.x + padIn, pOrigin.y + padIn); const float rowW = w - 2.0f * padIn; diff --git a/src/ui/windows/key_export_dialog.cpp b/src/ui/windows/key_export_dialog.cpp index fa3f01c..cb65482 100644 --- a/src/ui/windows/key_export_dialog.cpp +++ b/src/ui/windows/key_export_dialog.cpp @@ -309,12 +309,10 @@ void KeyExportDialog::render(App* app) } } - ImGui::Spacing(); - - // Close button + // Close button — centered via the shared footer helper (no divider, matching the + // dialog's prior leading-Spacing placement) instead of hand-computing the offset. float button_width = closeBtn.width; - float avail_width = ImGui::GetContentRegionAvail().x; - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (avail_width - button_width) / 2.0f); + material::BeginOverlayDialogFooter(button_width, /*drawSeparator=*/false); if (material::TactileButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) { s_open = false; diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index cdc2ffe..d064ae0 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -2149,8 +2149,7 @@ static void mktDrawPortfolio(const MktCtx& cx) ImDrawList* rdl = ImGui::GetWindowDrawList(); float rowW = ImGui::GetContentRegionAvail().x; // narrows automatically if a scrollbar shows if (vis.empty()) { - ImGui::Dummy(ImVec2(0, Layout::spacingSm())); - Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries")); + material::DrawEmptyState(ICON_MD_PIE_CHART, TR("portfolio_no_entries")); } else { // TABLE column-header strip (style 0): a thin static header labelling the numeric columns — // a signature the card styles don't have. Uses the shared pfTableCols so it aligns with rows. diff --git a/src/ui/windows/peers_tab.cpp b/src/ui/windows/peers_tab.cpp index 036c268..b1ecb62 100644 --- a/src/ui/windows/peers_tab.cpp +++ b/src/ui/windows/peers_tab.cpp @@ -621,11 +621,9 @@ void RenderPeersTab(App* app) if (!s_show_banned) { // ---- Connected Peers ---- if (!app->isConnected()) { - ImGui::Dummy(ImVec2(0, 20)); - Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("not_connected")); + material::DrawEmptyState(ICON_MD_CLOUD_OFF, TR("not_connected")); } else if (state.peers.empty()) { - ImGui::Dummy(ImVec2(0, 20)); - Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("peers_no_connected")); + material::DrawEmptyState(ICON_MD_WIFI_TETHERING, TR("peers_no_connected")); } else { const float dp = ui::Layout::dpiScale(); float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg(); @@ -827,11 +825,9 @@ void RenderPeersTab(App* app) } else { // ---- Banned Peers ---- if (!app->isConnected()) { - ImGui::Dummy(ImVec2(0, 20)); - Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("not_connected")); + material::DrawEmptyState(ICON_MD_CLOUD_OFF, TR("not_connected")); } else if (state.bannedPeers.empty()) { - ImGui::Dummy(ImVec2(0, 20)); - Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("peers_no_banned")); + material::DrawEmptyState(ICON_MD_BLOCK, TR("peers_no_banned")); } else { const float dp = ui::Layout::dpiScale(); float rowH = capFont->LegacySize + S.drawElement("tabs.peers", "banned-row-height-padding").size * dp; diff --git a/src/ui/windows/qr_popup_dialog.cpp b/src/ui/windows/qr_popup_dialog.cpp index 7e40ea3..809b852 100644 --- a/src/ui/windows/qr_popup_dialog.cpp +++ b/src/ui/windows/qr_popup_dialog.cpp @@ -120,8 +120,10 @@ void QRPopupDialog::render(App* app) widgets::AddressCopyField("##QRAddress", s_address); ImGui::Spacing(); - - // Buttons — size each to its label (so "Copy address" never clips), then center the pair. + + // Footer — a primary "Copy address" + "Close" pair, placed via the shared design-system helper. + // Size both buttons to the wider label (so "Copy address" never clips) and hand that width to + // DialogActionFooter, which centers the pair. ImFont* btnFont = S.resolveFont(actionBtn.font); ImGui::PushFont(btnFont); const float btnPad = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f; @@ -130,14 +132,14 @@ void QRPopupDialog::render(App* app) ImGui::PopFont(); if (w_copy < actionBtn.width) w_copy = actionBtn.width; if (w_close < actionBtn.width) w_close = actionBtn.width; - const float total_width = w_copy + w_close + ImGui::GetStyle().ItemSpacing.x; - ImGui::SetCursorPosX((window_width - total_width) / 2.0f); + const float btnW = (w_copy > w_close ? w_copy : w_close); - if (material::TactileButton(TR("copy_address"), ImVec2(w_copy, 0), btnFont)) { + bool doCopy = false, doClose = false; + material::DialogActionFooter(TR("copy_address"), true, TR("close"), doCopy, doClose, btnW); + if (doCopy) { ImGui::SetClipboardText(s_address.c_str()); } - ImGui::SameLine(); - if (material::TactileButton(TR("close"), ImVec2(w_close, 0), btnFont)) { + if (doClose) { close(); } material::EndOverlayDialog(); diff --git a/src/ui/windows/request_payment_dialog.cpp b/src/ui/windows/request_payment_dialog.cpp index ccaae6e..f63293c 100644 --- a/src/ui/windows/request_payment_dialog.cpp +++ b/src/ui/windows/request_payment_dialog.cpp @@ -224,6 +224,10 @@ void RequestPaymentDialog::render(App* app) if (w_uri < actionBtn.width) w_uri = actionBtn.width; if (w_addr < actionBtn.width) w_addr = actionBtn.width; + // Center the two-button copy row via the shared helper (no extra divider; the Separator + // above already frames this region). Total = both button widths + the ItemSpacing between. + material::BeginOverlayDialogFooter(w_uri + w_addr + ImGui::GetStyle().ItemSpacing.x, false); + // Copy URI button if (material::TactileButton(TR("request_copy_uri"), ImVec2(w_uri, 0), btnFont)) { ImGui::SetClipboardText(s_payment_uri.c_str()); @@ -248,6 +252,9 @@ void RequestPaymentDialog::render(App* app) float w_close = ImGui::CalcTextSize(TR("close")).x + btnPad; ImGui::PopFont(); if (w_close < actionBtn.width) w_close = actionBtn.width; + // Center the lone Close button via the shared helper (no extra divider — matches the + // existing tight spacing above it). + material::BeginOverlayDialogFooter(w_close, false); if (material::TactileButton(TR("close"), ImVec2(w_close, 0), btnFont)) { s_open = false; } diff --git a/src/ui/windows/shield_dialog.cpp b/src/ui/windows/shield_dialog.cpp index 488f524..e3477f8 100644 --- a/src/ui/windows/shield_dialog.cpp +++ b/src/ui/windows/shield_dialog.cpp @@ -184,9 +184,16 @@ void ShieldDialog::render(App* app) bool sh_syncing = state.sync.syncing; bool can_submit = !s_operation_pending && s_to_address[0] != '\0' && sh_connected && !sh_syncing; - if (!can_submit) ImGui::BeginDisabled(); - + // Center the primary + Cancel action row via the shared footer helper. We can't use + // DialogActionFooter here because the primary button carries a disabled-hover tooltip that must + // fire on ITS item (the helper draws primary+Close internally, leaving no hook between them), so + // we keep the two TactileButtons + the interleaved tooltip and only standardize the placement. const char* btn_label = (s_mode == Mode::ShieldCoinbase) ? TR("shield_funds") : TR("merge_funds"); + float footerBtnW = shieldBtn.width + cancelBtn.width + ImGui::GetStyle().ItemSpacing.x; + material::BeginOverlayDialogFooter(footerBtnW, /*drawSeparator=*/false); + + if (!can_submit) ImGui::BeginDisabled(); + if (material::TactileButton(btn_label, ImVec2(shieldBtn.width, 0), S.resolveFont(shieldBtn.font))) { s_operation_pending = true; s_status_message = TR("shield_submitting"); diff --git a/src/ui/windows/transaction_details_dialog.cpp b/src/ui/windows/transaction_details_dialog.cpp index d1f4635..77121d6 100644 --- a/src/ui/windows/transaction_details_dialog.cpp +++ b/src/ui/windows/transaction_details_dialog.cpp @@ -174,25 +174,18 @@ void TransactionDetailsDialog::render(App* app) ImGui::Separator(); ImGui::Spacing(); - // Buttons - float button_width = bottomBtn.width; - float total_width = button_width * 2 + ImGui::GetStyle().ItemSpacing.x; - float start_x = (ImGui::GetWindowWidth() - total_width) / 2.0f; - ImGui::SetCursorPosX(start_x); - + // Buttons — centered primary + Close pair via the shared footer helper. // Guard against an empty/whitespace explorer URL so we never open a garbage link. std::string explorerBase = app->settings()->getTxExplorerUrl(); bool explorerValid = explorerBase.find_first_not_of(" \t\r\n") != std::string::npos; - if (!explorerValid) ImGui::BeginDisabled(); - if (material::StyledButton(TR("tx_view_explorer"), ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) { + bool doExplorer = false, doClose = false; + material::DialogActionFooter(TR("tx_view_explorer"), explorerValid, TR("close"), + doExplorer, doClose, bottomBtn.width); + if (doExplorer) { std::string url = explorerBase + tx.txid; util::Platform::openUrl(url); } - if (!explorerValid) ImGui::EndDisabled(); - - ImGui::SameLine(); - - if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) { + if (doClose) { s_open = false; } material::EndOverlayDialog(); diff --git a/src/ui/windows/transactions_tab.cpp b/src/ui/windows/transactions_tab.cpp index 728d2f9..21fbc9f 100644 --- a/src/ui/windows/transactions_tab.cpp +++ b/src/ui/windows/transactions_tab.cpp @@ -541,32 +541,22 @@ void RenderTransactionsTab(App* app) float startX = ImGui::GetContentRegionMax().x - totalPagW; ImGui::SetCursorPosX(startX); - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm()); - ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.5f, 0.5f)); - ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15))); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 30))); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 45))); - - // First page + // First page — TactileButton (shared glass press, matches the explorer pager) ImGui::BeginDisabled(s_current_page == 0); - ImGui::PushFont(Type().iconSmall()); - if (ImGui::Button(ICON_MD_FIRST_PAGE "##txFirst", ImVec2(btnW, btnW))) { + if (TactileButton(ICON_MD_FIRST_PAGE "##txFirst", ImVec2(btnW, btnW), Type().iconSmall())) { s_current_page = 0; s_expanded_row = -1; } - ImGui::PopFont(); ImGui::EndDisabled(); ImGui::SameLine(0, gap); // Previous page ImGui::BeginDisabled(s_current_page == 0); - ImGui::PushFont(Type().iconSmall()); - if (ImGui::Button(ICON_MD_CHEVRON_LEFT "##txPrev", ImVec2(btnW, btnW))) { + if (TactileButton(ICON_MD_CHEVRON_LEFT "##txPrev", ImVec2(btnW, btnW), Type().iconSmall())) { s_current_page--; s_expanded_row = -1; } - ImGui::PopFont(); ImGui::EndDisabled(); ImGui::SameLine(0, gap); @@ -590,28 +580,21 @@ void RenderTransactionsTab(App* app) // Next page ImGui::BeginDisabled(s_current_page >= totalPages - 1); - ImGui::PushFont(Type().iconSmall()); - if (ImGui::Button(ICON_MD_CHEVRON_RIGHT "##txNext", ImVec2(btnW, btnW))) { + if (TactileButton(ICON_MD_CHEVRON_RIGHT "##txNext", ImVec2(btnW, btnW), Type().iconSmall())) { s_current_page++; s_expanded_row = -1; } - ImGui::PopFont(); ImGui::EndDisabled(); ImGui::SameLine(0, gap); // Last page ImGui::BeginDisabled(s_current_page >= totalPages - 1); - ImGui::PushFont(Type().iconSmall()); - if (ImGui::Button(ICON_MD_LAST_PAGE "##txLast", ImVec2(btnW, btnW))) { + if (TactileButton(ICON_MD_LAST_PAGE "##txLast", ImVec2(btnW, btnW), Type().iconSmall())) { s_current_page = totalPages - 1; s_expanded_row = -1; } - ImGui::PopFont(); ImGui::EndDisabled(); - - ImGui::PopStyleColor(3); - ImGui::PopStyleVar(2); } } ImGui::Dummy(ImVec2(0, Layout::spacingXs())); @@ -641,20 +624,17 @@ void RenderTransactionsTab(App* app) ImGui::Dummy(ImVec2(0, Layout::spacingSm())); { if (!app->isConnected()) { - ImGui::Dummy(ImVec2(0, 20)); - Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), - TR(app->isLiteBuild() ? "lite_no_wallet" : "not_connected")); + material::DrawEmptyState(ICON_MD_CLOUD_OFF, + TR(app->isLiteBuild() ? "lite_no_wallet" : "not_connected")); } else if (state.transactions.empty()) { - ImGui::Dummy(ImVec2(0, 20)); if (txLoading) { snprintf(buf, sizeof(buf), "%s%s", txLoadingText.c_str(), material::LoadingDots()); - Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf); + material::DrawEmptyState(ICON_MD_HOURGLASS_EMPTY, buf); } else { - Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions")); + material::DrawEmptyState(ICON_MD_RECEIPT_LONG, TR("no_transactions")); } } else if (filtered_indices.empty()) { - ImGui::Dummy(ImVec2(0, 20)); - Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_matching")); + material::DrawEmptyState(ICON_MD_SEARCH_OFF, TR("no_matching")); } else { float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd(); float innerW = ImGui::GetContentRegionAvail().x; diff --git a/src/ui/windows/validate_address_dialog.cpp b/src/ui/windows/validate_address_dialog.cpp index 5a17f96..7f7ad4a 100644 --- a/src/ui/windows/validate_address_dialog.cpp +++ b/src/ui/windows/validate_address_dialog.cpp @@ -210,11 +210,10 @@ void ValidateAddressDialog::render(App* app) ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("not_connected")); } - ImGui::Spacing(); - - // Close button at bottom + // Close button at bottom — centered via the shared footer helper (no separator, matching + // the prior hand-rolled placement). float button_width = closeBtn.width; - ImGui::SetCursorPosX((ImGui::GetWindowWidth() - button_width) / 2.0f); + material::BeginOverlayDialogFooter(button_width, /*drawSeparator=*/false); if (material::TactileButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) { s_open = false; } diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 46a1010..641cab8 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -951,7 +951,7 @@ void I18n::loadBuiltinEnglish() strings_["filter"] = "Filter..."; strings_["no_addresses_yet"] = "No addresses yet"; strings_["showing_x_of_y"] = "Showing %d of %d addresses"; - strings_["set_label"] = "Set Label..."; + strings_["set_label"] = "Set Label"; strings_["copied"] = "Copied!"; strings_["hidden_tag"] = " (hidden)"; strings_["z_address"] = "Z-Address";