refactor(ui): consolidate dialog footers + shared empty-state, unify buttons/rounding

Addresses the cross-screen inconsistencies from the layout audit by routing screens onto
the design system's own (previously under-used) shared helpers:

- Dialog footers: migrate ~9 overlay dialogs off hand-rolled placement onto the shared
  helpers — DialogActionFooter (primary+Close), DialogConfirmFooter, or
  BeginOverlayDialogFooter for custom/multi-button rows — so footers share one centered
  treatment. All footer/action buttons now use TactileButton (glass press) instead of the
  bare StyledButton some dialogs used.
- Empty states: add a shared material::DrawEmptyState(icon, title, hint) (centered icon +
  title + wrapped hint) and adopt it in Peers, Transactions, and Market-portfolio, which
  previously showed a bare left-aligned caption.
- Security dialogs: add the missing Cancel to Change Passphrase and Set PIN so the whole
  security family shares a two-button footer (Cancel dismisses without applying).
- Transactions pager: shared TactileButton helpers (matching Explorer).
- Frosted-pane rounding: Contacts/Chat use Layout::glassRounding() instead of hardcoded
  12/10/8px literals, matching Peers.
- "Set Label..." title loses its stray trailing ellipsis.

Preserves every button's label and action; the transfer footer's order becomes
[Confirm][Cancel] to match the shared helper's primary-first convention. Verified at 1280
across full-node + Lite + Windows (ctest green) and an adversarial diff review (clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-19 11:23:49 -05:00
parent a598217975
commit d5c30237d2
25 changed files with 177 additions and 126 deletions

View File

@@ -1312,7 +1312,7 @@
"sent_filter": "Gesendet", "sent_filter": "Gesendet",
"sent_type": "Gesendet", "sent_type": "Gesendet",
"sent_upper": "GESENDET", "sent_upper": "GESENDET",
"set_label": "Label setzen...", "set_label": "Label setzen",
"settings": "Einstellungen", "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_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:", "settings_acrylic_level": "Acrylstufe:",

View File

@@ -1312,7 +1312,7 @@
"sent_filter": "Enviado", "sent_filter": "Enviado",
"sent_type": "Enviado", "sent_type": "Enviado",
"sent_upper": "ENVIADO", "sent_upper": "ENVIADO",
"set_label": "Establecer Etiqueta...", "set_label": "Establecer Etiqueta",
"settings": "Ajustes", "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_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:", "settings_acrylic_level": "Nivel de acrílico:",

View File

@@ -1312,7 +1312,7 @@
"sent_filter": "Envoyé", "sent_filter": "Envoyé",
"sent_type": "Envoyé", "sent_type": "Envoyé",
"sent_upper": "ENVOYÉ", "sent_upper": "ENVOYÉ",
"set_label": "Définir le libellé...", "set_label": "Définir le libellé",
"settings": "Paramètres", "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_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 :", "settings_acrylic_level": "Niveau acrylique :",

View File

@@ -1309,7 +1309,7 @@
"sent_filter": "送信済み", "sent_filter": "送信済み",
"sent_type": "送信済み", "sent_type": "送信済み",
"sent_upper": "送信済み", "sent_upper": "送信済み",
"set_label": "ラベルを設定...", "set_label": "ラベルを設定",
"settings": "設定", "settings": "設定",
"settings_about_text": "DragonX (DRGX) 用のシールド暗号通貨ウォレット。Dear ImGui で構築された軽量でポータブルな体験。", "settings_about_text": "DragonX (DRGX) 用のシールド暗号通貨ウォレット。Dear ImGui で構築された軽量でポータブルな体験。",
"settings_acrylic_level": "アクリルレベル:", "settings_acrylic_level": "アクリルレベル:",

View File

@@ -1311,7 +1311,7 @@
"sent_filter": "전송됨", "sent_filter": "전송됨",
"sent_type": "전송됨", "sent_type": "전송됨",
"sent_upper": "전송됨", "sent_upper": "전송됨",
"set_label": "라벨 설정...", "set_label": "라벨 설정",
"settings": "설정", "settings": "설정",
"settings_about_text": "DragonX (DRGX)용 차폐 암호화폐 지갑으로, Dear ImGui로 제작되어 가볍고 휴대 가능합니다.", "settings_about_text": "DragonX (DRGX)용 차폐 암호화폐 지갑으로, Dear ImGui로 제작되어 가볍고 휴대 가능합니다.",
"settings_acrylic_level": "아크릴 레벨:", "settings_acrylic_level": "아크릴 레벨:",

View File

@@ -1312,7 +1312,7 @@
"sent_filter": "Enviado", "sent_filter": "Enviado",
"sent_type": "Enviado", "sent_type": "Enviado",
"sent_upper": "ENVIADO", "sent_upper": "ENVIADO",
"set_label": "Definir Rótulo...", "set_label": "Definir Rótulo",
"settings": "Ajustes", "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_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:", "settings_acrylic_level": "Nível acrílico:",

View File

@@ -1312,7 +1312,7 @@
"sent_filter": "Отправлено", "sent_filter": "Отправлено",
"sent_type": "Отправлено", "sent_type": "Отправлено",
"sent_upper": "ОТПРАВЛЕНО", "sent_upper": "ОТПРАВЛЕНО",
"set_label": "Установить метку...", "set_label": "Установить метку",
"settings": "Настройки", "settings": "Настройки",
"settings_about_text": "Защищённый криптовалютный кошелёк для DragonX (DRGX), созданный на Dear ImGui для лёгкого и портативного использования.", "settings_about_text": "Защищённый криптовалютный кошелёк для DragonX (DRGX), созданный на Dear ImGui для лёгкого и портативного использования.",
"settings_acrylic_level": "Уровень акрила:", "settings_acrylic_level": "Уровень акрила:",

View File

@@ -1310,7 +1310,7 @@
"sent_filter": "已发送", "sent_filter": "已发送",
"sent_type": "已发送", "sent_type": "已发送",
"sent_upper": "已发送", "sent_upper": "已发送",
"set_label": "设置标签...", "set_label": "设置标签",
"settings": "设置", "settings": "设置",
"settings_about_text": "DragonX (DRGX) 屏蔽加密货币钱包,使用 Dear ImGui 构建,提供轻量、便携的体验。", "settings_about_text": "DragonX (DRGX) 屏蔽加密货币钱包,使用 Dear ImGui 构建,提供轻量、便携的体验。",
"settings_acrylic_level": "亚克力级别:", "settings_acrylic_level": "亚克力级别:",

View File

@@ -1476,12 +1476,22 @@ void App::renderEncryptWalletDialog() {
bool valid = strlen(change_old_pass_buf_) > 0 && bool valid = strlen(change_old_pass_buf_) > 0 &&
strlen(change_new_pass_buf_) >= 8 && strlen(change_new_pass_buf_) >= 8 &&
strcmp(change_new_pass_buf_, change_confirm_buf_) == 0; 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_); 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_), changePassphrase(std::string(change_old_pass_buf_),
std::string(change_new_pass_buf_)); std::string(change_new_pass_buf_));
} }
ImGui::EndDisabled(); 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(); EndOverlayDialog();
} }
@@ -1945,8 +1955,10 @@ void App::renderPinDialogs() {
util::SecureVault::isValidPin(pinStr) && util::SecureVault::isValidPin(pinStr) &&
strcmp(pin_buf_, pin_confirm_buf_) == 0; 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_); 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_in_progress_ = true;
pin_status_ = "Verifying passphrase..."; pin_status_ = "Verifying passphrase...";
@@ -2005,6 +2017,13 @@ void App::renderPinDialogs() {
} }
} }
ImGui::EndDisabled(); 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(); EndOverlayDialog();
} }
// Wipe the passphrase/PIN buffers if the dialog was dismissed (X / Esc / // Wipe the passphrase/PIN buffers if the dialog was dismissed (X / Esc /

View File

@@ -79,6 +79,63 @@ inline const char* LoadingDots() {
return kDots[n]; 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 // Text Drop Shadow
// ============================================================================ // ============================================================================

View File

@@ -125,29 +125,26 @@ void RenderAboutDialog(App* app, bool* p_open)
ImGui::Spacing(); ImGui::Spacing();
ImGui::TextWrapped("%s", TR("about_license_text")); ImGui::TextWrapped("%s", TR("about_license_text"));
ImGui::Spacing(); // Links — 3-button action row, centered via the shared footer helper (draws its own
ImGui::Separator(); // Spacing/Separator/Spacing above the row, replacing the hand-rolled divider block).
ImGui::Spacing(); const float linksTotalW = linkW * 3.0f + ImGui::GetStyle().ItemSpacing.x * 2.0f;
material::BeginOverlayDialogFooter(linksTotalW);
// Links if (material::TactileButton(TR("about_website"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
if (material::StyledButton(TR("about_website"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) {
util::Platform::openUrl("https://dragonx.is"); util::Platform::openUrl("https://dragonx.is");
} }
ImGui::SameLine(); 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"); util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon");
} }
ImGui::SameLine(); 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"); util::Platform::openUrl("https://explorer.dragonx.is");
} }
ImGui::Spacing(); // Close button — lone dismiss action, centered via the shared footer helper (no extra
// divider above it, so it sits directly under the links row).
// Close button material::BeginOverlayDialogFooter(closeW, false);
float button_width = closeW; if (material::TactileButton(TR("close"), ImVec2(closeW, 0), S.resolveFont(closeBtn.font))) {
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - button_width) * 0.5f);
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
*p_open = false; *p_open = false;
} }

View File

@@ -180,16 +180,14 @@ public:
const char* sendingLabel = TR("sending"); const char* sendingLabel = TR("sending");
ImFont* buttonFont = Type().button(); ImFont* buttonFont = Type().button();
float buttonFontSize = ScaledFontSize(buttonFont); float buttonFontSize = ScaledFontSize(buttonFont);
float minBtnW = 120.0f * dp;
float confirmMinW = 160.0f * dp; float confirmMinW = 160.0f * dp;
float buttonPadW = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp; float buttonPadW = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp;
float cancelW = std::max(minBtnW, // Both footer buttons share one width (equal-width primary/Close pair), sized to fit the
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, cancelLabel).x + buttonPadW); // widest label — the "Sending…" swap label included — so nothing clips.
float confirmTextW = std::max( float confirmTextW = std::max(
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, confirmLabel).x, buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, confirmLabel).x,
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, sendingLabel).x); buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, sendingLabel).x);
float confirmW = std::max(confirmMinW, confirmTextW + buttonPadW); float btnW = std::max(confirmMinW, confirmTextW + buttonPadW);
float totalW = cancelW + confirmW + Layout::spacingMd();
float footerH = ImGui::GetFrameHeight() + ImGui::GetStyle().ItemSpacing.y * 3.0f; // footer divider removed float footerH = ImGui::GetFrameHeight() + ImGui::GetStyle().ItemSpacing.y * 3.0f; // footer divider removed
ImGuiViewport* vp = ImGui::GetMainViewport(); ImGuiViewport* vp = ImGui::GetMainViewport();
float cardBottomY = vp->Pos.y + vp->Size.y * 0.85f; float cardBottomY = vp->Pos.y + vp->Size.y * 0.85f;
@@ -201,19 +199,18 @@ public:
ImGui::Spacing(); 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(); if (outClose) {
float contentW = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalW) * 0.5f));
if (TactileButton(cancelLabel, ImVec2(cancelW, 0), buttonFont)) {
s_open = false; s_open = false;
} }
ImGui::SameLine(0, Layout::spacingMd()); if (outConfirm) {
ImGui::BeginDisabled(!amountValid || s_sending);
if (TactileButton(s_sending ? sendingLabel : confirmLabel, ImVec2(confirmW, 0), buttonFont)) {
s_sending = true; s_sending = true;
s_app->sendTransaction(s_info.fromAddr, s_info.toAddr, s_app->sendTransaction(s_info.fromAddr, s_info.toAddr,
amount, s_fee, "", amount, s_fee, "",
@@ -231,7 +228,6 @@ public:
// state, and when the async callback sets s_resultMsg the in-dialog result screen shows // 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. // (with its own Close button). Previously closing here made that result screen dead code.
} }
ImGui::EndDisabled();
EndOverlayDialog(); EndOverlayDialog();
} }

View File

@@ -135,7 +135,7 @@ void BlockInfoDialog::render(App* app)
ImGui::BeginDisabled(); 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()) { if (rpc && rpc->isConnected() && app->worker()) {
s_loading = true; s_loading = true;
s_error.clear(); s_error.clear();
@@ -303,7 +303,7 @@ void BlockInfoDialog::render(App* app)
// Navigation buttons // Navigation buttons
if (s_has_data) { if (s_has_data) {
if (s_height > 1) { 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_height--;
s_has_data = false; s_has_data = false;
s_error.clear(); s_error.clear();
@@ -315,7 +315,7 @@ void BlockInfoDialog::render(App* app)
// nextblockhash, so this stays hidden there). // nextblockhash, so this stays hidden there).
if (!s_next_hash.empty() && if (!s_next_hash.empty() &&
(state.sync.blocks <= 0 || s_height < state.sync.blocks)) { (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_height++;
s_has_data = false; s_has_data = false;
s_error.clear(); s_error.clear();
@@ -323,9 +323,10 @@ void BlockInfoDialog::render(App* app)
} }
} }
// Close button at bottom // Close button at bottom — centered via the shared footer helper (no separator, matching
ImGui::SetCursorPosY(ImGui::GetWindowHeight() - 40); // the prior hand-rolled placement).
if (material::StyledButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) { material::BeginOverlayDialogFooter(closeBtn.width, /*drawSeparator=*/false);
if (material::TactileButton(TR("close"), ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false; s_open = false;
} }
material::EndOverlayDialog(); material::EndOverlayDialog();

View File

@@ -451,7 +451,7 @@ void RenderChatSettingsPreview(App* app, float width) {
totalH += padIn; totalH += padIn;
const ImVec2 origin = ImGui::GetCursorScreenPos(); 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); material::DrawGlassPanel(dl, origin, ImVec2(origin.x + width, origin.y + totalH), g);
const float leftX = origin.x + padIn; const float leftX = origin.x + padIn;
@@ -795,7 +795,7 @@ void RenderChatTab(App* app)
{ {
ImDrawList* paneDL = ImGui::GetWindowDrawList(); ImDrawList* paneDL = ImGui::GetWindowDrawList();
const ImVec2 pMin = ImGui::GetCursorScreenPos(); 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); 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 // 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(); ImDrawList* paneDL = ImGui::GetWindowDrawList();
const ImVec2 pMin = ImGui::GetCursorScreenPos(); const ImVec2 pMin = ImGui::GetCursorScreenPos();
const float pW = ImGui::GetContentRegionAvail().x; 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); 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 // 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. // FrameBg); a flat FrameBg showed the sharp texture.
{ {
ImDrawList* cdl = ImGui::GetWindowDrawList(); 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), material::DrawGlassPanel(cdl, ImVec2(inputX, rowY),
ImVec2(inputX + inputW, rowY + composerBoxH), g); ImVec2(inputX + inputW, rowY + composerBoxH), g);
} }

View File

@@ -1026,7 +1026,7 @@ void RenderContactsTab(App* app)
const float tpW = ImGui::GetContentRegionAvail().x; const float tpW = ImGui::GetContentRegionAvail().x;
const float tIpad = 12.0f * dp, tVpad = 10.0f * dp; 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); material::DrawGlassPanel(tpdl, tpMin, ImVec2(tpMin.x + tpW, tpMin.y + listH), g);
} }
ImGui::SetCursorScreenPos(ImVec2(tpMin.x + tIpad, tpMin.y + tVpad)); ImGui::SetCursorScreenPos(ImVec2(tpMin.x + tIpad, tpMin.y + tVpad));
@@ -1129,7 +1129,7 @@ void RenderContactsTab(App* app)
ImDrawList* pdl = ImGui::GetWindowDrawList(); ImDrawList* pdl = ImGui::GetWindowDrawList();
const ImVec2 pMin = ImGui::GetCursorScreenPos(); const ImVec2 pMin = ImGui::GetCursorScreenPos();
const float pW = ImGui::GetContentRegionAvail().x; 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); material::DrawGlassPanel(pdl, pMin, ImVec2(pMin.x + pW, pMin.y + listH), g);
} }
// AlwaysUseWindowPadding: a borderless child ignores WindowPadding without it, so the rows // 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 rowH = avR * 2.0f + 16.0f * dp;
const float panelH = 2.0f * rowH + gap + 2.0f * padIn; const float panelH = 2.0f * rowH + gap + 2.0f * padIn;
const ImVec2 pOrigin = ImGui::GetCursorScreenPos(); 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); material::DrawGlassPanel(pdl, pOrigin, ImVec2(pOrigin.x + w, pOrigin.y + panelH), g);
const ImVec2 rmn(pOrigin.x + padIn, pOrigin.y + padIn); const ImVec2 rmn(pOrigin.x + padIn, pOrigin.y + padIn);
const float rowW = w - 2.0f * padIn; const float rowW = w - 2.0f * padIn;

View File

@@ -309,12 +309,10 @@ void KeyExportDialog::render(App* app)
} }
} }
ImGui::Spacing(); // Close button — centered via the shared footer helper (no divider, matching the
// dialog's prior leading-Spacing placement) instead of hand-computing the offset.
// Close button
float button_width = closeBtn.width; float button_width = closeBtn.width;
float avail_width = ImGui::GetContentRegionAvail().x; material::BeginOverlayDialogFooter(button_width, /*drawSeparator=*/false);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (avail_width - button_width) / 2.0f);
if (material::TactileButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) { if (material::TactileButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
s_open = false; s_open = false;

View File

@@ -2149,8 +2149,7 @@ static void mktDrawPortfolio(const MktCtx& cx)
ImDrawList* rdl = ImGui::GetWindowDrawList(); ImDrawList* rdl = ImGui::GetWindowDrawList();
float rowW = ImGui::GetContentRegionAvail().x; // narrows automatically if a scrollbar shows float rowW = ImGui::GetContentRegionAvail().x; // narrows automatically if a scrollbar shows
if (vis.empty()) { if (vis.empty()) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm())); material::DrawEmptyState(ICON_MD_PIE_CHART, TR("portfolio_no_entries"));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
} else { } else {
// TABLE column-header strip (style 0): a thin static header labelling the numeric columns — // 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. // a signature the card styles don't have. Uses the shared pfTableCols so it aligns with rows.

View File

@@ -621,11 +621,9 @@ void RenderPeersTab(App* app)
if (!s_show_banned) { if (!s_show_banned) {
// ---- Connected Peers ---- // ---- Connected Peers ----
if (!app->isConnected()) { if (!app->isConnected()) {
ImGui::Dummy(ImVec2(0, 20)); material::DrawEmptyState(ICON_MD_CLOUD_OFF, TR("not_connected"));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("not_connected"));
} else if (state.peers.empty()) { } else if (state.peers.empty()) {
ImGui::Dummy(ImVec2(0, 20)); material::DrawEmptyState(ICON_MD_WIFI_TETHERING, TR("peers_no_connected"));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("peers_no_connected"));
} else { } else {
const float dp = ui::Layout::dpiScale(); const float dp = ui::Layout::dpiScale();
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg(); float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg();
@@ -827,11 +825,9 @@ void RenderPeersTab(App* app)
} else { } else {
// ---- Banned Peers ---- // ---- Banned Peers ----
if (!app->isConnected()) { if (!app->isConnected()) {
ImGui::Dummy(ImVec2(0, 20)); material::DrawEmptyState(ICON_MD_CLOUD_OFF, TR("not_connected"));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("not_connected"));
} else if (state.bannedPeers.empty()) { } else if (state.bannedPeers.empty()) {
ImGui::Dummy(ImVec2(0, 20)); material::DrawEmptyState(ICON_MD_BLOCK, TR("peers_no_banned"));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("peers_no_banned"));
} else { } else {
const float dp = ui::Layout::dpiScale(); const float dp = ui::Layout::dpiScale();
float rowH = capFont->LegacySize + S.drawElement("tabs.peers", "banned-row-height-padding").size * dp; float rowH = capFont->LegacySize + S.drawElement("tabs.peers", "banned-row-height-padding").size * dp;

View File

@@ -121,7 +121,9 @@ void QRPopupDialog::render(App* app)
ImGui::Spacing(); 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); ImFont* btnFont = S.resolveFont(actionBtn.font);
ImGui::PushFont(btnFont); ImGui::PushFont(btnFont);
const float btnPad = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f; const float btnPad = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f;
@@ -130,14 +132,14 @@ void QRPopupDialog::render(App* app)
ImGui::PopFont(); ImGui::PopFont();
if (w_copy < actionBtn.width) w_copy = actionBtn.width; if (w_copy < actionBtn.width) w_copy = actionBtn.width;
if (w_close < actionBtn.width) w_close = actionBtn.width; if (w_close < actionBtn.width) w_close = actionBtn.width;
const float total_width = w_copy + w_close + ImGui::GetStyle().ItemSpacing.x; const float btnW = (w_copy > w_close ? w_copy : w_close);
ImGui::SetCursorPosX((window_width - total_width) / 2.0f);
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::SetClipboardText(s_address.c_str());
} }
ImGui::SameLine(); if (doClose) {
if (material::TactileButton(TR("close"), ImVec2(w_close, 0), btnFont)) {
close(); close();
} }
material::EndOverlayDialog(); material::EndOverlayDialog();

View File

@@ -224,6 +224,10 @@ void RequestPaymentDialog::render(App* app)
if (w_uri < actionBtn.width) w_uri = actionBtn.width; if (w_uri < actionBtn.width) w_uri = actionBtn.width;
if (w_addr < actionBtn.width) w_addr = 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 // Copy URI button
if (material::TactileButton(TR("request_copy_uri"), ImVec2(w_uri, 0), btnFont)) { if (material::TactileButton(TR("request_copy_uri"), ImVec2(w_uri, 0), btnFont)) {
ImGui::SetClipboardText(s_payment_uri.c_str()); 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; float w_close = ImGui::CalcTextSize(TR("close")).x + btnPad;
ImGui::PopFont(); ImGui::PopFont();
if (w_close < actionBtn.width) w_close = actionBtn.width; 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)) { if (material::TactileButton(TR("close"), ImVec2(w_close, 0), btnFont)) {
s_open = false; s_open = false;
} }

View File

@@ -184,9 +184,16 @@ void ShieldDialog::render(App* app)
bool sh_syncing = state.sync.syncing; bool sh_syncing = state.sync.syncing;
bool can_submit = !s_operation_pending && s_to_address[0] != '\0' && sh_connected && !sh_syncing; bool can_submit = !s_operation_pending && s_to_address[0] != '\0' && sh_connected && !sh_syncing;
// 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 (!can_submit) ImGui::BeginDisabled();
const char* btn_label = (s_mode == Mode::ShieldCoinbase) ? TR("shield_funds") : TR("merge_funds");
if (material::TactileButton(btn_label, ImVec2(shieldBtn.width, 0), S.resolveFont(shieldBtn.font))) { if (material::TactileButton(btn_label, ImVec2(shieldBtn.width, 0), S.resolveFont(shieldBtn.font))) {
s_operation_pending = true; s_operation_pending = true;
s_status_message = TR("shield_submitting"); s_status_message = TR("shield_submitting");

View File

@@ -174,25 +174,18 @@ void TransactionDetailsDialog::render(App* app)
ImGui::Separator(); ImGui::Separator();
ImGui::Spacing(); ImGui::Spacing();
// Buttons // Buttons — centered primary + Close pair via the shared footer helper.
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);
// Guard against an empty/whitespace explorer URL so we never open a garbage link. // Guard against an empty/whitespace explorer URL so we never open a garbage link.
std::string explorerBase = app->settings()->getTxExplorerUrl(); std::string explorerBase = app->settings()->getTxExplorerUrl();
bool explorerValid = explorerBase.find_first_not_of(" \t\r\n") != std::string::npos; bool explorerValid = explorerBase.find_first_not_of(" \t\r\n") != std::string::npos;
if (!explorerValid) ImGui::BeginDisabled(); bool doExplorer = false, doClose = false;
if (material::StyledButton(TR("tx_view_explorer"), ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) { material::DialogActionFooter(TR("tx_view_explorer"), explorerValid, TR("close"),
doExplorer, doClose, bottomBtn.width);
if (doExplorer) {
std::string url = explorerBase + tx.txid; std::string url = explorerBase + tx.txid;
util::Platform::openUrl(url); util::Platform::openUrl(url);
} }
if (!explorerValid) ImGui::EndDisabled(); if (doClose) {
ImGui::SameLine();
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
s_open = false; s_open = false;
} }
material::EndOverlayDialog(); material::EndOverlayDialog();

View File

@@ -541,32 +541,22 @@ void RenderTransactionsTab(App* app)
float startX = ImGui::GetContentRegionMax().x - totalPagW; float startX = ImGui::GetContentRegionMax().x - totalPagW;
ImGui::SetCursorPosX(startX); ImGui::SetCursorPosX(startX);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm()); // First page — TactileButton (shared glass press, matches the explorer pager)
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
ImGui::BeginDisabled(s_current_page == 0); ImGui::BeginDisabled(s_current_page == 0);
ImGui::PushFont(Type().iconSmall()); if (TactileButton(ICON_MD_FIRST_PAGE "##txFirst", ImVec2(btnW, btnW), Type().iconSmall())) {
if (ImGui::Button(ICON_MD_FIRST_PAGE "##txFirst", ImVec2(btnW, btnW))) {
s_current_page = 0; s_current_page = 0;
s_expanded_row = -1; s_expanded_row = -1;
} }
ImGui::PopFont();
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::SameLine(0, gap); ImGui::SameLine(0, gap);
// Previous page // Previous page
ImGui::BeginDisabled(s_current_page == 0); ImGui::BeginDisabled(s_current_page == 0);
ImGui::PushFont(Type().iconSmall()); if (TactileButton(ICON_MD_CHEVRON_LEFT "##txPrev", ImVec2(btnW, btnW), Type().iconSmall())) {
if (ImGui::Button(ICON_MD_CHEVRON_LEFT "##txPrev", ImVec2(btnW, btnW))) {
s_current_page--; s_current_page--;
s_expanded_row = -1; s_expanded_row = -1;
} }
ImGui::PopFont();
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::SameLine(0, gap); ImGui::SameLine(0, gap);
@@ -590,28 +580,21 @@ void RenderTransactionsTab(App* app)
// Next page // Next page
ImGui::BeginDisabled(s_current_page >= totalPages - 1); ImGui::BeginDisabled(s_current_page >= totalPages - 1);
ImGui::PushFont(Type().iconSmall()); if (TactileButton(ICON_MD_CHEVRON_RIGHT "##txNext", ImVec2(btnW, btnW), Type().iconSmall())) {
if (ImGui::Button(ICON_MD_CHEVRON_RIGHT "##txNext", ImVec2(btnW, btnW))) {
s_current_page++; s_current_page++;
s_expanded_row = -1; s_expanded_row = -1;
} }
ImGui::PopFont();
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::SameLine(0, gap); ImGui::SameLine(0, gap);
// Last page // Last page
ImGui::BeginDisabled(s_current_page >= totalPages - 1); ImGui::BeginDisabled(s_current_page >= totalPages - 1);
ImGui::PushFont(Type().iconSmall()); if (TactileButton(ICON_MD_LAST_PAGE "##txLast", ImVec2(btnW, btnW), Type().iconSmall())) {
if (ImGui::Button(ICON_MD_LAST_PAGE "##txLast", ImVec2(btnW, btnW))) {
s_current_page = totalPages - 1; s_current_page = totalPages - 1;
s_expanded_row = -1; s_expanded_row = -1;
} }
ImGui::PopFont();
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::PopStyleColor(3);
ImGui::PopStyleVar(2);
} }
} }
ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
@@ -641,20 +624,17 @@ void RenderTransactionsTab(App* app)
ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
{ {
if (!app->isConnected()) { if (!app->isConnected()) {
ImGui::Dummy(ImVec2(0, 20)); material::DrawEmptyState(ICON_MD_CLOUD_OFF,
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR(app->isLiteBuild() ? "lite_no_wallet" : "not_connected"));
TR(app->isLiteBuild() ? "lite_no_wallet" : "not_connected"));
} else if (state.transactions.empty()) { } else if (state.transactions.empty()) {
ImGui::Dummy(ImVec2(0, 20));
if (txLoading) { if (txLoading) {
snprintf(buf, sizeof(buf), "%s%s", txLoadingText.c_str(), material::LoadingDots()); 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 { } else {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions")); material::DrawEmptyState(ICON_MD_RECEIPT_LONG, TR("no_transactions"));
} }
} else if (filtered_indices.empty()) { } else if (filtered_indices.empty()) {
ImGui::Dummy(ImVec2(0, 20)); material::DrawEmptyState(ICON_MD_SEARCH_OFF, TR("no_matching"));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_matching"));
} else { } else {
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd(); float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd();
float innerW = ImGui::GetContentRegionAvail().x; float innerW = ImGui::GetContentRegionAvail().x;

View File

@@ -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::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("not_connected"));
} }
ImGui::Spacing(); // Close button at bottom — centered via the shared footer helper (no separator, matching
// the prior hand-rolled placement).
// Close button at bottom
float button_width = closeBtn.width; 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))) { if (material::TactileButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
s_open = false; s_open = false;
} }

View File

@@ -951,7 +951,7 @@ void I18n::loadBuiltinEnglish()
strings_["filter"] = "Filter..."; strings_["filter"] = "Filter...";
strings_["no_addresses_yet"] = "No addresses yet"; strings_["no_addresses_yet"] = "No addresses yet";
strings_["showing_x_of_y"] = "Showing %d of %d addresses"; strings_["showing_x_of_y"] = "Showing %d of %d addresses";
strings_["set_label"] = "Set Label..."; strings_["set_label"] = "Set Label";
strings_["copied"] = "Copied!"; strings_["copied"] = "Copied!";
strings_["hidden_tag"] = " (hidden)"; strings_["hidden_tag"] = " (hidden)";
strings_["z_address"] = "Z-Address"; strings_["z_address"] = "Z-Address";