3 Commits

Author SHA1 Message Date
8778398d31 feat(ui): layout polish — fill dead space, pair/box/collapse, warning weight, dialog glass
Implements the layout-improvement suggestions from the layout audit (visual arrangement
only; no functionality added or removed):

- Send/Receive: the recent-activity list now grows to fill the space below the fixed
  compose/receive card (more history visible), with a centered empty-state when there is
  none — instead of leaving dead canvas.
- Shield/Merge: pair the Fee and UTXO-Limit fields on one row to tighten vertical rhythm.
- Market: extend + frame the portfolio group-list as one contained panel (with a bottom
  edge) and center its empty-state, closing the previously un-anchored gap.
- Overlay dialogs: raise the card glass fill/border alpha (35/50 -> 60/90 of 255) so the
  dialog card reads as a distinct surface over busy backdrops (global, all overlays).
- Wallets: size the list height to the actual wallet count instead of always reserving 7
  rows, removing the large gap before the scan/create prompts (still scrolls when many).
- Contacts: width-aware address truncation shows more of the address on wide rows.
- Transfer Funds: give the "sends the full balance" warning a warning icon + color so the
  stakes stand out from the neutral result-preview lines.
- First-run wizard: collapse a completed Step 1 (Appearance) to the compact pill like
  Step 2, so a finished step is no longer taller than the active one.
- Explorer: distribute the Chain card's two stats to match the density of the sibling
  metrics grid.
- Validate Address: a "Results will appear here" caption fills the pre-interaction blank.
- Change Passphrase: add the warning banner its sibling security dialogs have.
- Migration ShowSeed: box the 24-word mnemonic grid (a GlassSectionScope behind the
  existing RenderSeedWordGrid) so the critical secret reads as a distinct artifact —
  purely visual, no seed/logic/state change.

Verified at 1280 across full-node + Lite + Windows (ctest green) and an adversarial diff
review (clean). New i18n key backfilled into all 8 languages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-19 13:56:13 -05:00
d5c30237d2 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>
2026-08-19 11:23:49 -05:00
a598217975 fix(ui): cut-off/clipping — recent-tx collisions, updater note wrap, request-payment URI, console filter, icon-grid scroll
Fixes the cut-off/clipping bugs from the layout audit (all visible at the default
1280/1024 window sizes):

- Receive "Recent Received" rows: the amount collided with the relative-time
  ("+15.7500 DRGX14 days ago") and the type label touched the address at narrow
  widths. Use the shared short time format (formatTimeAgoShort, matching Overview),
  chain the amount's right edge off the measured time width, and start the address
  after the measured type-label width — so neither pair can collide.
- Daemon & xmrig updater verify-note: drawn unwrapped and clipped at the card's right
  edge; wrap it (PushTextWrapPos) within the already-reserved height.
- Request Payment: the three footer buttons shared one fixed width (clipping "Copy
  Full Address"); size each to its own label. The Payment URI overflowed a plain
  field; render it in a bordered read-only box (bounded, un-chunked).
- Console: the filter input shrank below its own placeholder (gone entirely at 1024);
  give it a min width >= the placeholder and drop the "N lines" count when the row
  can't fit both.
- Address-label "Choose Icon" grid: had NoScrollbar hiding most of the catalog with
  no cue; give it a real scrollbar.
- Overview "Recent Transactions": drop the 4th row at 1024 (it clipped off-screen) by
  capping to rows that fully fit the reserved height.
- Sidebar: reserve the unread-badge width in the nav-label centering so History/Chat
  labels no longer collide with their badge.

Verified at 1024 and 1280 across full-node + Lite + Windows (ctest green) and an
adversarial diff review (clean). Skipped the legacy settings_window overlay footer
(dead code / removal candidate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-19 10:11:55 -05:00
37 changed files with 494 additions and 238 deletions

View File

@@ -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:",
@@ -1672,6 +1672,7 @@
"validate_not_mine": "Nicht im Besitz dieser Wallet",
"validate_ownership": "Eigentum:",
"validate_results": "Ergebnisse:",
"validate_results_placeholder": "Ergebnisse erscheinen hier",
"validate_shielded_type": "Abgeschirmt (z-Adresse)",
"validate_status": "Status:",
"validate_title": "Adresse validieren",

View File

@@ -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:",
@@ -1672,6 +1672,7 @@
"validate_not_mine": "No es propiedad de esta cartera",
"validate_ownership": "Propiedad:",
"validate_results": "Resultados:",
"validate_results_placeholder": "Los resultados aparecerán aquí",
"validate_shielded_type": "Protegida (dirección z)",
"validate_status": "Estado:",
"validate_title": "Validar Dirección",

View File

@@ -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 :",
@@ -1672,6 +1672,7 @@
"validate_not_mine": "N'appartient pas à ce portefeuille",
"validate_ownership": "Propriété :",
"validate_results": "Résultats :",
"validate_results_placeholder": "Les résultats apparaîtront ici",
"validate_shielded_type": "Blindée (z-adresse)",
"validate_status": "Statut :",
"validate_title": "Valider l'adresse",

View File

@@ -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": "アクリルレベル:",
@@ -1669,6 +1669,7 @@
"validate_not_mine": "このウォレットに属していません",
"validate_ownership": "所有者:",
"validate_results": "結果:",
"validate_results_placeholder": "ここに結果が表示されます",
"validate_shielded_type": "シールドzアドレス",
"validate_status": "ステータス:",
"validate_title": "アドレスを検証",

View File

@@ -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": "아크릴 레벨:",
@@ -1671,6 +1671,7 @@
"validate_not_mine": "이 지갑에 속하지 않음",
"validate_ownership": "소유자:",
"validate_results": "결과:",
"validate_results_placeholder": "결과가 여기에 표시됩니다",
"validate_shielded_type": "차폐 (z 주소)",
"validate_status": "상태:",
"validate_title": "주소 검증",

View File

@@ -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:",
@@ -1672,6 +1672,7 @@
"validate_not_mine": "Não pertence a esta carteira",
"validate_ownership": "Propriedade:",
"validate_results": "Resultados:",
"validate_results_placeholder": "Os resultados aparecerão aqui",
"validate_shielded_type": "Blindado (z-endereço)",
"validate_status": "Status:",
"validate_title": "Validar Endereço",

View File

@@ -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": "Уровень акрила:",
@@ -1672,6 +1672,7 @@
"validate_not_mine": "Не принадлежит этому кошельку",
"validate_ownership": "Принадлежность:",
"validate_results": "Результаты:",
"validate_results_placeholder": "Результаты появятся здесь",
"validate_shielded_type": "Экранированный (z-адрес)",
"validate_status": "Статус:",
"validate_title": "Проверить адрес",

View File

@@ -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": "亚克力级别:",
@@ -1670,6 +1670,7 @@
"validate_not_mine": "不属于此钱包",
"validate_ownership": "所有权:",
"validate_results": "结果:",
"validate_results_placeholder": "结果将显示在此处",
"validate_shielded_type": "屏蔽z 地址)",
"validate_status": "状态:",
"validate_title": "验证地址",

View File

@@ -4174,7 +4174,12 @@ void App::renderSeedMigrationDialog()
case SeedMigrationStep::ShowSeed: {
ui::material::DialogWarningHeader(TR("mig_seed_warning"));
ImGui::Spacing();
// Box the 24-word grid in a glass panel so the wallet's most critical secret reads as a
// distinct artifact, not body copy. Purely visual — the words/derivation/logic are untouched.
{
ui::material::GlassSectionScope seedPanel;
ui::RenderSeedWordGrid(ui::SplitSeedWords(seed_migration_seed_));
}
ImGui::Spacing();
ImGui::TextColored(kMedium, "%s", TR("mig_receive_addr"));
ImGui::TextWrapped("%s", seed_migration_dest_.c_str());

View File

@@ -1450,6 +1450,11 @@ void App::renderEncryptWalletDialog() {
ov.cardWidth = 460.0f; ov.idSuffix = "changepass";
if (BeginOverlayDialog(ov)) {
// Same fund-loss consequence as Encrypt/Remove Encryption if the new
// passphrase is lost — reuse their warning string/header for consistency.
DialogWarningHeader(TR("wiz_encrypt_warning"));
ImGui::Spacing();
ImGui::TextUnformatted(TR("change_pass_current"));
ImGui::PushItemWidth(-1);
ImGui::InputText("##chg_old", change_old_pass_buf_, sizeof(change_old_pass_buf_),
@@ -1476,12 +1481,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 +1960,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 +2022,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 /

View File

@@ -294,10 +294,23 @@ void App::renderFirstRunWizard() {
{
int state = cardState(0);
bool isFocused = (state == 1);
bool isCollapsed = (state == 2); // Completed: minimize to a compact pill (mirrors Card 1)
float cx = leftX + cardPad;
float cy = card0Top + cardPad;
float contentW = colW - 2 * cardPad;
// Step indicator + title (inline when collapsed)
if (isCollapsed) {
// Compact single-line: check icon + "Step 1" + "Appearance"
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
float labelX = cx + iconW + 4.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(labelX, cy), dimCol, TR("wiz_step1"));
float step1W = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, TR("wiz_step1")).x;
float titleX = labelX + step1W + 12.0f * dp;
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(titleX, cy), dimCol, TR("wiz_appearance"));
cy += captionFont->LegacySize + 4.0f * dp;
} else {
// Step indicator
{
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
@@ -317,6 +330,7 @@ void App::renderFirstRunWizard() {
dl->AddLine(ImVec2(cx, cy), ImVec2(cx + contentW, cy),
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
cy += 14.0f * dp;
}
float& wiz_blur_amount = wizardUi.blur_amount;
bool& wiz_theme_effects = wizardUi.theme_effects;
@@ -352,6 +366,9 @@ void App::renderFirstRunWizard() {
wiz_appearance_init = true;
}
// Controls: rendered for the focused and upcoming states so content is visible under
// the dim overlay; skipped entirely once completed so the card shrinks to a compact pill.
if (!isCollapsed) {
// Render controls always so content is visible under the dim
// overlay when not focused; disable interaction when not active.
ImGui::BeginDisabled(!isFocused);
@@ -668,14 +685,22 @@ void App::renderFirstRunWizard() {
cy += btnH;
}
} // if (!isCollapsed)
cy += cardPad;
// Lock card height to the tallest content ever seen
// Lock card height to the tallest content ever seen (but not when collapsed)
float& card0MaxH = wizardUi.card0_max_h;
if (isCollapsed) {
// Completed: finalize immediately as a compact pill (do not stretch to the
// right column height, and skip the deferred stretch below).
card0Bot = card0Top + (cy - card0Top);
finalizeCard(leftX, colW, card0Top, card0Bot, state);
} else {
card0MaxH = std::max(card0MaxH, cy - card0Top);
card0Bot = card0Top + card0MaxH;
// Card 0 finalization deferred until after cards 1+2 are sized
}
}
// ======================= CARD 1: Bootstrap =======================
@@ -1447,7 +1472,9 @@ void App::renderFirstRunWizard() {
}
// --- Deferred Card 0 finalization: match right column total height ---
{
// Only for the focused/upcoming Appearance card; a completed one was already finalized
// above as a compact pill and must not be re-stretched.
if (cardState(0) != 2) {
float rightColBot = card2Bot;
if (rightColBot > card0Bot) card0Bot = rightColBot;
finalizeCard(leftX, colW, card0Top, card0Bot, cardState(0));

View File

@@ -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
// ============================================================================
@@ -1583,7 +1640,10 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
// the measuring frame (its geometry is a placeholder; the whole card is hidden until centered).
if (!floating && !hideForMeasure) {
GlassPanelSpec cardGlass;
cardGlass.rounding = 16.0f; cardGlass.fillAlpha = 35; cardGlass.borderAlpha = 50; cardGlass.borderWidth = 1.0f;
// Fill/border alpha govern every overlay dialog's card boundary — kept well above the default
// glass panel so the card reads as a distinct surface over busy backdrops (tx lists, mining
// tiles, chat) while staying translucent rather than opaque.
cardGlass.rounding = 16.0f; cardGlass.fillAlpha = 60; cardGlass.borderAlpha = 90; cardGlass.borderWidth = 1.0f;
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
}

View File

@@ -676,17 +676,33 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
ImU32 textCol = selected ? Primary() : (pageNeedsUnlock ? OnSurfaceDisabled() : OnSurfaceMedium());
if (showLabels) {
// Reserve room for a badge (if this item will draw one) so the
// label centers in the space to the left of it instead of
// running underneath the badge circle.
bool itemHasBadge =
(item.page == NavPage::History && status.unconfirmedTxCount > 0) ||
(item.page == NavPage::Mining && status.miningActive) ||
(item.page == NavPage::Peers && status.peerCount > 0) ||
(item.page == NavPage::Chat && status.chatUnreadCount > 0);
float badgeReserve = 0.0f;
if (itemHasBadge) {
bool dotOnlyReserve = (item.page == NavPage::Mining);
float badgeRReserve = dotOnlyReserve ? badgeRadiusDot : badgeRadiusNumber;
float badgeInsetXReserve = sde("badge-inset-x", 6.0f);
badgeReserve = badgeRReserve * 2.0f + badgeInsetXReserve;
}
ImFont* font = selected ? Type().subtitle2() : Type().body2();
float lblFsz = ScaledFontSize(font);
float btnW = indMax.x - indMin.x;
float maxLabelW = btnW - iconS * 2.0f - iconLabelGap - Layout::spacingXs() * 2;
float maxLabelW = btnW - iconS * 2.0f - iconLabelGap - Layout::spacingXs() * 2 - badgeReserve;
ImVec2 labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item));
if (labelSz.x > maxLabelW && maxLabelW > 0) {
lblFsz *= maxLabelW / labelSz.x;
labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item));
}
float totalW = iconS * 2.0f + iconLabelGap + labelSz.x;
float btnCX = (indMin.x + indMax.x) * 0.5f;
float btnCX = (indMin.x + indMax.x - badgeReserve) * 0.5f;
float startX = btnCX - totalW * 0.5f;
DrawNavIcon(dl, item.page, startX + iconS, iconCY, iconS, textCol);

View File

@@ -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;
}

View File

@@ -138,8 +138,13 @@ public:
const float controlsTopY = std::max(gridStartY + cellSz * 2.0f, buttonY - preButtonReserve);
const float gridMaxH = std::max(cellSz * 2.0f, controlsTopY - gridStartY);
ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(0, 0, 0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 11.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 5.5f * dp);
// Scrollbar visible (not NoScrollbar) — the icon set exceeds the fixed-height
// grid, so a real scrollbar is the discoverable way to reach the rest.
ImGui::BeginChild("##IconGrid", ImVec2(avail, gridMaxH), ImGuiChildFlags_None,
ImGuiWindowFlags_NoScrollbar);
ImGuiWindowFlags_NoScrollWithMouse);
ApplySmoothScroll();
ImDrawList* dl = ImGui::GetWindowDrawList();
@@ -185,6 +190,7 @@ public:
}
ImGui::EndChild();
ImGui::PopStyleVar(2); // ScrollbarSize + ScrollbarRounding
ImGui::PopStyleColor();
if (ImGui::GetCursorPosY() < controlsTopY) {

View File

@@ -169,9 +169,9 @@ public:
}
if (amountValid && newFromBal < 1e-9) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
ImGui::TextWrapped("%s", TR("sends_full_balance_warning"));
ImGui::PopStyleColor();
// Full-balance send: same warning-icon treatment as the de-shielding header above,
// so this stakes-bearing line reads as distinct from the neutral preview text.
DialogWarningHeader(TR("sends_full_balance_warning"));
}
// Buttons
@@ -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();
}

View File

@@ -805,6 +805,7 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
const float kRecentTxRowHeight = S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f);
const auto& state = app->state();
float headerStartY = ImGui::GetCursorPosY();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("recent_transactions"));
ImGui::SameLine();
@@ -812,6 +813,7 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
app->setCurrentPage(NavPage::History);
}
ImGui::Spacing();
float headerHeight = ImGui::GetCursorPosY() - headerStartY;
float scaledRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs);
float availableListH = ImGui::GetContentRegionAvail().y;
@@ -820,14 +822,17 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
ImGuiWindowFlags_NoBackground);
const auto& txs = state.transactions;
int count = std::min(4, (int)txs.size()); // show only the 4 most recent (state.transactions is newest-first)
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
// Only draw as many rows as fully fit within the reserved section height (header + rows);
// dropping the overflow row is fine since "View All" already links to full History.
int maxRows = std::max(1, (int)((recentH - headerHeight) / rowH));
int count = std::min({4, maxRows, (int)txs.size()}); // show only the most recent (state.transactions is newest-first)
if (count == 0) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions_yet"));
} else {
ImDrawList* dl = ImGui::GetWindowDrawList();
ImFont* capFont = Type().caption();
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size,
S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -564,12 +564,27 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
ImGui::SameLine();
}
// Line count
ImGui::TextDisabled(TR("console_line_count"), model_.size());
// Line count — the least-critical trailing element. When the row is too narrow to fit the
// filter box (at its placeholder-sized minimum) AND its trailing controls, drop the line count
// rather than starve/hide the filter (worst at 1024px). Mirror the reservation formula in
// drawFilterInput(): trailing = 4 frame-height buttons + the group spacers, and the filter's
// hard floor = its placeholder width + frame padding.
{
char lineCountBuf[64];
snprintf(lineCountBuf, sizeof(lineCountBuf), TR("console_line_count"), model_.size());
float lineCountW = ImGui::CalcTextSize(lineCountBuf).x + Layout::spacingSm() * 2.0f; // text + its trailing spacer
float trailingW = ImGui::GetFrameHeight() * 4.0f + Layout::spacingSm() * 7.0f;
float filterMinW = ImGui::CalcTextSize(TR("console_filter_hint")).x
+ ImGui::GetStyle().FramePadding.x * 2.0f + 8.0f * Layout::dpiScale();
bool showLineCount = ImGui::GetContentRegionAvail().x >= lineCountW + trailingW + filterMinW;
if (showLineCount) {
ImGui::TextDisabled(TR("console_line_count"), model_.size());
ImGui::SameLine();
ImGui::Spacing();
ImGui::SameLine();
}
}
// Output filter input
drawFilterInput();
@@ -695,6 +710,13 @@ void ConsoleTab::drawFilterInput()
float filterAvail = ImGui::GetContentRegionAvail().x - trailingBtnSpace;
float filterMaxW = schema::UI().drawElement("tabs.console", "filter-max-width").size * Layout::dpiScale();
float filterW = std::min(filterMaxW, filterAvail * schema::UI().drawElement("tabs.console", "filter-width-ratio").size);
// Never shrink below the placeholder — otherwise the hint clips to "Filter outp" (or the box
// vanishes) at narrow widths. Floor = placeholder text + frame padding + a little breathing room.
// (drawToolbar() drops the "NNN lines" count when even this floor won't fit alongside the row's
// trailing controls, so this max() doesn't push the zoom/color buttons off-window.)
float filterMinW = ImGui::CalcTextSize(TR("console_filter_hint")).x
+ ImGui::GetStyle().FramePadding.x * 2.0f + 8.0f * Layout::dpiScale();
filterW = std::max(filterMinW, filterW);
ImGui::SetNextItemWidth(filterW);
ImGui::InputTextWithHint("##ConsoleFilter", TR("console_filter_hint"), filter_text_, sizeof(filter_text_));
if (filter_text_[0] != '\0') {

View File

@@ -207,6 +207,28 @@ static bool isShieldedAddr(const std::string& a) {
return !a.empty() && a[0] == 'z';
}
// Width-aware middle-ellipsis truncation (mirrors the add/edit dialog's local fitMiddle lambda, but
// file-scope so the Cards/List rows can share it too): keeps head + tail, shrinking symmetrically
// until the rendered width fits maxW. minFront/minBack are the schema-configured floor — below that
// the fixed-length util::truncateMiddle result is used instead, so very cramped rows still read as
// "front...back" rather than collapsing to a near-empty stub. Addresses are ASCII, so byte-wise
// trimming is safe.
static std::string truncateAddressToWidth(const std::string& s, ImFont* f, float size, float maxW,
int minFront, int minBack) {
const std::string floor = util::truncateMiddle(s, minFront, minBack);
auto w = [&](const std::string& t){ return f->CalcTextSizeA(size, FLT_MAX, 0, t.c_str()).x; };
if (w(s) <= maxW) return s; // fits in full — no truncation needed at all
if (maxW <= 0.0f) return floor; // no room to measure against — fall back to the floor
const std::string ell = "\xE2\x80\xA6";
size_t head = s.size() / 2, tail = s.size() - head;
while (head + tail > static_cast<size_t>(minFront + minBack)) {
std::string cand = s.substr(0, head) + ell + s.substr(s.size() - tail);
if (w(cand) <= maxW) return cand;
if (head >= tail) --head; else --tail;
}
return floor; // couldn't fit even at the configured floor — use the fixed-length result
}
// Accent colour for a contact's address type (Z = shielded/green, T = transparent/amber), tuned per
// theme. File-scope so both the list rows and the edit-dialog preview share one source of truth.
static ImU32 contactTypeColor(bool shielded, bool light) {
@@ -1026,7 +1048,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 +1151,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
@@ -1219,10 +1241,13 @@ void RenderContactsTab(App* app)
dl->AddText(lblF, lblSz, ImVec2(tx, ty), material::OnSurface(), entry.label.c_str());
dl->PopClipRect();
// Un-collapse to the full address on hover (clipped to the text column so it never
// runs under the trailing actions); middle-truncated otherwise.
// runs under the trailing actions); otherwise middle-truncated to FIT the actual text
// column width (tx..textMaxX) rather than a fixed char count — wide rows show more of
// the address instead of leaving a dead gap before the action-icon cluster.
std::string addr = rowHovered
? entry.address
: util::truncateMiddle(entry.address, addrFrontLbl.truncate, addrBackLbl.truncate);
: truncateAddressToWidth(entry.address, adrF, adrSz, textMaxX - tx,
addrFrontLbl.truncate, addrBackLbl.truncate);
dl->PushClipRect(ImVec2(tx, mn.y), ImVec2(textMaxX, mx.y), true);
dl->AddText(adrF, adrSz, ImVec2(tx, ty + lblSz + 3.0f * dp),
material::OnSurfaceMedium(), addr.c_str());
@@ -1390,7 +1415,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;

View File

@@ -408,7 +408,9 @@ private:
// ---- Below the info card (outside the surface): verify note + install button ----
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x);
Type().textColored(TypeStyle::Caption, downgrade ? Warning() : OnSurfaceMedium(), noteStr);
ImGui::PopTextWrapPos();
ImGui::Spacing();
// Install button sized to its text and centered in the pane.
const float bw = ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x * 2.0f + Layout::spacingLg();

View File

@@ -620,7 +620,16 @@ static void renderChainStats(App* app, float availWidth) {
ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("explorer_chain_stats"));
drawStatusPill(cardMin, cardW);
float labelY = cardMin.y + pad * 0.5f + headerH + Layout::spacingLg();
// Distribute the two stat blocks (Height, Best Block) evenly through the
// content region so the card matches the density of the sibling 2x2 metric
// grid instead of pinning Height to the top and Best Block to the bottom
// edge with a large empty gap between them.
float contentTop = cardMin.y + pad * 0.5f + headerH;
float contentBottom = cardMax.y - pad;
float blockGap = std::max(Layout::spacingMd(),
(contentBottom - contentTop - heroLineH - hashLineH) / 3.0f);
float labelY = contentTop + blockGap;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cardMin.x + pad, labelY), OnSurfaceMedium(), TR("explorer_block_height"));
@@ -640,7 +649,7 @@ static void renderChainStats(App* app, float availWidth) {
ImVec2(cardMin.x + pad + barW * progress, barY + barH), WithAlpha(Warning(), 180), barH * 0.5f);
}
float hashLabelY = cardMax.y - pad - hashLineH;
float hashLabelY = labelY + heroLineH + blockGap;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cardMin.x + pad, hashLabelY), OnSurfaceMedium(), TR("peers_best_block"));

View File

@@ -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;

View File

@@ -2136,11 +2136,29 @@ static void mktDrawPortfolio(const MktCtx& cx)
float rowGap = pfRowGapFor(style, mktDp);
float rowsH = std::max(rowH, portfolioH - pfSummaryH);
// Anchor the bottom of the portfolio: the group list otherwise ends at its content height,
// leaving un-anchored dead space between the last row and the content-area bottom. Grow the
// rows region to fill the remaining vertical space (down to the scroll area's bottom, less a
// small margin) and wrap it in a contained glass panel — so it reads as one framed table with
// a real bottom edge (like Explorer's block-list card) instead of trailing off into nothing.
// The empty state then centres in this full-height panel rather than in a one-row band.
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pfSummaryH));
float floorH = ImGui::GetContentRegionAvail().y - gap; // to the scroll bottom, keep a margin
float panelH = std::max(rowsH, floorH);
{
ImVec2 pMin(cardMin.x, cardMin.y + pfSummaryH);
ImVec2 pMax(rightEdge, pMin.y + panelH);
GlassPanelSpec pg;
pg.rounding = Layout::glassRounding();
pg.fillAlpha = 12; pg.borderAlpha = 24; // faint container: frames without competing with the per-row cards
DrawGlassPanel(dl, pMin, pMax, pg);
}
// Flush-left child (no window padding) so rows align with the summary; a scrollbar appears
// only when the visible groups overflow the bounded height.
// only when the visible groups overflow the bounded height. NoBackground so the contained
// glass panel drawn above is the sole surface — otherwise the opaque ChildBg (non-acrylic
// mode) would paint over the panel fill + border.
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::BeginChild("##pfRows", ImVec2(availWidth, rowsH), false);
ImGui::BeginChild("##pfRows", ImVec2(availWidth, panelH), false, ImGuiWindowFlags_NoBackground);
ImGui::PopStyleVar();
// Zero item-spacing INSIDE the child: each row emits an InvisibleButton + a gap Dummy, and
// ImGui's default ItemSpacing.y between those items would inflate the content past rowsH and
@@ -2149,8 +2167,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.

View File

@@ -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;

View File

@@ -121,7 +121,9 @@ void QRPopupDialog::render(App* app)
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();

View File

@@ -324,20 +324,9 @@ static void RenderAddressDropdown(App* app, float width) {
}
// ============================================================================
// Helpers: timeAgo / DrawRecvIcon (local copies — originals are static in send_tab)
// Helpers: DrawRecvIcon (local copy — original is static in send_tab).
// Relative time uses the shared util::formatTimeAgoShort ("14d ago"), matching Overview/Send.
// ============================================================================
static std::string recvTimeAgo(int64_t timestamp) {
if (timestamp <= 0) return "";
int64_t now = (int64_t)std::time(nullptr);
int64_t diff = now - timestamp;
if (diff < 0) diff = 0;
char buf[32];
if (diff < 60) { snprintf(buf, sizeof(buf), TR("time_seconds_ago"), (long long)diff); return buf; }
if (diff < 3600) { snprintf(buf, sizeof(buf), TR("time_minutes_ago"), (long long)(diff / 60)); return buf; }
if (diff < 86400) { snprintf(buf, sizeof(buf), TR("time_hours_ago"), (long long)(diff / 3600)); return buf; }
snprintf(buf, sizeof(buf), TR("time_days_ago"), (long long)(diff / 86400)); return buf;
}
static void DrawRecvIcon(ImDrawList* dl, float cx, float cy, float s, ImU32 col) {
dl->AddTriangleFilled(
ImVec2(cx, cy + s),
@@ -373,9 +362,14 @@ static void RenderRecentReceived(const AddressInfo& /* addr */,
if (tx.type != "receive" && tx.type != "mined") continue;
recvs.push_back(&tx);
}
if (recvs.size() > 4) recvs.resize(4); // show only the 4 most recent (newest-first)
// Grow the list to fill the dead space beneath the (fixed-height) receive card:
// fit as many newest-first rows as the remaining region can show, instead of a
// fixed 4. The child scrolls if the real history exceeds what fits, so nothing is
// lost. A floor of 4 keeps the section substantial when the region is short.
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
size_t maxRows = std::max<size_t>(4, (size_t)std::floor(listH / rowH));
if (recvs.size() > maxRows) recvs.resize(maxRows); // newest-first
ImGui::BeginChild("##RecentReceivedRows", ImVec2(width, listH), false,
ImGuiWindowFlags_NoBackground);
@@ -383,8 +377,9 @@ static void RenderRecentReceived(const AddressInfo& /* addr */,
char buf[64];
if (recvs.empty()) {
ImGui::SetCursorPosY(Layout::spacingMd());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_recent_receives"));
// Fill the empty canvas with a centered material empty-state (icon + title)
// rather than a lone left-aligned caption stranded at the top of dead space.
material::DrawEmptyState(ICON_MD_CALL_RECEIVED, TR("no_recent_receives"));
ImGui::EndChild();
return;
}
@@ -404,30 +399,37 @@ static void RenderRecentReceived(const AddressInfo& /* addr */,
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(txX, rowPos.y + 2.0f * dp), OnSurfaceMedium(), typeText);
// Address (second line)
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f) * hs;
// Address — start it AFTER the measured type-label width (not a fixed offset that shrinks below
// the label at narrow widths), mirroring Overview's recent-tx list. The schema offset is a floor.
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, typeText).x;
float addrX = txX + std::max(S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f) * hs,
typeW + Layout::spacingSm());
std::string addrDisplay = util::truncateMiddle(tx.address,
(int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str());
// Amount (right-aligned, first line)
// Time ago — short "14d ago" form (shared helper, matches Overview/Send). Measure it
// first so the amount can chain its right edge off this width and never overlap.
std::string ago = util::formatTimeAgoShort(tx.timestamp);
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, ago.c_str());
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
float agoMargin = S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f) * hs;
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(rightEdge - agoSz.x - agoMargin, rowPos.y + 2.0f * dp),
OnSurfaceDisabled(), ago.c_str());
// Amount (right-aligned, first line) — anchored to the LEFT of the time-ago text
// (measured width + a gap) so the two columns can never collide, whatever the strings.
snprintf(buf, sizeof(buf), "+%.4f %s", std::abs(tx.amount), DRAGONX_TICKER);
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, buf);
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
float amtX = rightEdge - amtSz.x - std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size,
float amtGap = std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size,
S.drawElement("tabs.balance", "amount-right-margin").size * hs);
float amtRightEdge = rightEdge - agoSz.x - agoMargin - amtGap;
float amtX = amtRightEdge - amtSz.x;
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(amtX, rowPos.y + 2.0f * dp), recvCol, buf);
// Time ago
std::string ago = recvTimeAgo(tx.timestamp);
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, ago.c_str());
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f) * hs,
rowPos.y + 2.0f * dp),
OnSurfaceDisabled(), ago.c_str());
// Clickable row — hover highlight + navigate to History
float rowW = ImGui::GetContentRegionAvail().x;
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);

View File

@@ -9,6 +9,7 @@
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../widgets/qr_code.h"
#include "../widgets/copy_field.h"
#include "../material/draw_helpers.h"
#include "imgui.h"
@@ -198,23 +199,44 @@ void RequestPaymentDialog::render(App* app)
// Payment URI display
if (!s_payment_uri.empty()) {
// Use a selectable text area for the URI
char uri_buf[1024];
strncpy(uri_buf, s_payment_uri.c_str(), sizeof(uri_buf) - 1);
material::LabeledInput(TR("request_payment_uri"), "##URI", uri_buf, sizeof(uri_buf),
-1.0f, nullptr, ImGuiInputTextFlags_ReadOnly);
ImGui::Text("%s", TR("request_payment_uri"));
// Bordered read-only field: bounds the raw drgx: URI within a box (it horizontal-scrolls if
// long) rather than 4-char-chunking it like an address or letting a plain field overflow.
// The "Copy URI" button below copies the full value.
{
char uriBuf[2048];
snprintf(uriBuf, sizeof(uriBuf), "%s", s_payment_uri.c_str());
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
ImGui::InputText("##URI", uriBuf, sizeof(uriBuf), ImGuiInputTextFlags_ReadOnly);
}
ImGui::Spacing();
// Copy button
if (material::TactileButton(TR("request_copy_uri"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
// Footer buttons: size each to ITS OWN label (action-button.width is a floor, not the size)
// so "Copy Full Address" never clips its final letter — mirrors qr_popup_dialog's per-label
// sizing. Font metrics are already DPI-scaled, so don't multiply by dpiScale() here.
ImFont* btnFont = S.resolveFont(actionBtn.font);
ImGui::PushFont(btnFont);
const float btnPad = ImGui::GetStyle().FramePadding.x * 2.0f + 12.0f; // small margin
float w_uri = ImGui::CalcTextSize(TR("request_copy_uri")).x + btnPad;
float w_addr = ImGui::CalcTextSize(TR("copy_address")).x + btnPad;
ImGui::PopFont();
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());
Notifications::instance().success(TR("request_uri_copied"));
}
ImGui::SameLine();
if (material::TactileButton(TR("copy_address"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
if (material::TactileButton(TR("copy_address"), ImVec2(w_addr, 0), btnFont)) {
ImGui::SetClipboardText(s_address);
Notifications::instance().success(TR("address_copied"));
}
@@ -222,10 +244,21 @@ void RequestPaymentDialog::render(App* app)
ImGui::Spacing();
// Close button
if (material::TactileButton(TR("close"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
// Close button — sized to its own label with the same floor.
{
ImFont* btnFont = S.resolveFont(actionBtn.font);
ImGui::PushFont(btnFont);
const float btnPad = ImGui::GetStyle().FramePadding.x * 2.0f + 12.0f;
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;
}
}
material::EndOverlayDialog();
}

View File

@@ -1123,9 +1123,14 @@ static void RenderRecentSends(const WalletState& state, float width, ImFont* cap
if (tx.type != "send" && tx.type != "shield") continue;
sends.push_back(&tx);
}
if (sends.size() > 4) sends.resize(4); // show only the 4 most recent (newest-first)
// Grow the list to fill the dead space beneath the (fixed-height) compose card:
// fit as many newest-first rows as the remaining region can show, instead of a
// fixed 4. The child scrolls if the real history exceeds what fits, so nothing is
// lost. A floor of 4 keeps the section substantial when the region is short.
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
size_t maxRows = std::max<size_t>(4, (size_t)std::floor(listH / rowH));
if (sends.size() > maxRows) sends.resize(maxRows); // newest-first
ImGui::BeginChild("##RecentSendRows", ImVec2(width, listH), false,
ImGuiWindowFlags_NoBackground);
@@ -1133,8 +1138,9 @@ static void RenderRecentSends(const WalletState& state, float width, ImFont* cap
char buf[64];
if (sends.empty()) {
ImGui::SetCursorPosY(Layout::spacingMd());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("send_no_recent"));
// Fill the empty canvas with a centered material empty-state (icon + title)
// rather than a lone left-aligned caption stranded at the top of dead space.
material::DrawEmptyState(ICON_MD_CALL_MADE, TR("send_no_recent"));
ImGui::EndChild();
return;
}

View File

@@ -146,7 +146,10 @@ void ShieldDialog::render(App* app)
ImGui::Spacing();
// Fee
// Fee + UTXO limit share one row (two columns) to tighten vertical rhythm.
float pairColX = ImGui::GetContentRegionAvail().x * 0.5f;
// Fee (left column)
ImGui::Text("%s", TR("fee_label"));
ImGui::SetNextItemWidth(feeInput.width * Layout::dpiScale());
ImGui::InputDouble("##Fee", &s_fee, 0.0001, 0.001, "%.8f");
@@ -155,16 +158,18 @@ void ShieldDialog::render(App* app)
ImGui::SameLine();
ImGui::TextDisabled("DRGX");
ImGui::Spacing();
// UTXO limit
// UTXO limit (right column) — hint drops under the input (rather than beside it) since
// "Max UTXOs per operation" is too long to share the narrower half-width column with "DRGX".
ImGui::SameLine(pairColX);
ImGui::BeginGroup();
ImGui::Text("%s", TR("shield_utxo_limit"));
ImGui::SetNextItemWidth(utxoInput.width * Layout::dpiScale());
ImGui::InputInt("##Limit", &s_utxo_limit);
ImGui::SameLine();
ImGui::TextDisabled("%s", TR("shield_max_utxos"));
if (s_utxo_limit < 1) s_utxo_limit = 1;
if (s_utxo_limit > 100) s_utxo_limit = 100;
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(),
TR("shield_max_utxos"));
ImGui::EndGroup();
ImGui::Spacing();
@@ -184,9 +189,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;
// 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();
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))) {
s_operation_pending = true;
s_status_message = TR("shield_submitting");

View File

@@ -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();

View File

@@ -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(),
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;

View File

@@ -208,13 +208,20 @@ void ValidateAddressDialog::render(App* app)
}
} else if (!app->isConnected()) {
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("not_connected"));
} else {
// Pre-interaction placeholder: keeps the results area from reading as a large
// dead gap between the Validate/Paste row and the Close button until a result exists.
ImGui::Spacing();
float cw = ImGui::GetContentRegionAvail().x;
ImVec2 ts = ImGui::CalcTextSize(TR("validate_results_placeholder"));
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (cw - ts.x) * 0.5f);
ImGui::TextDisabled("%s", TR("validate_results_placeholder"));
}
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;
}

View File

@@ -71,9 +71,12 @@ public:
const int kMaxVisibleRows = 7;
ImFont* nameFont = Type().subtitle1();
ImFont* metaFont = Type().caption();
// The list is always sized to its MAX height (kMaxVisibleRows) for a consistent modal size — it
// does not shrink to fit a few wallets; fewer rows leave empty space, more than 7 scroll.
const int visRows = kMaxVisibleRows;
// Size the list to the ACTUAL wallet count so a few wallets sit tight against the controls below
// instead of stranding ~200px of empty rows before the create/scan prompts; cap at kMaxVisibleRows
// (more than that scrolls within the cap). An empty list keeps two rows of height so the centered
// empty-state hint (drawn inside the list child) still has room to show.
const int numWallets = (int)s_rows.size();
const int visRows = numWallets <= 0 ? 2 : std::min(numWallets, kMaxVisibleRows);
const float cardPadY = Layout::spacingMd(); // roomier cards (was spacingSm)
const float walRowH = cardPadY * 2.0f + nameFont->LegacySize + Layout::spacingSm() + metaFont->LegacySize;
const float cardGap = Layout::spacingMd(); // more breathing room between wallet cards
@@ -421,9 +424,10 @@ public:
ImGui::Dummy(ImVec2(rowW, walRowH));
if (i + 1 < s_rows.size()) ImGui::Dummy(ImVec2(rowW, cardGap));
}
// Empty-state nudge: the list sits at a fixed max height, so a handful of wallets leave blank
// space below. When there's real room to spare, fill it with a subtle centered hint (a folder
// glyph + one line) instead of dead space; it's purely decorative — the actions live below.
// Empty-state nudge: the list is now sized to the actual wallet count, so a populated list has
// no room to spare and this draws nothing (the cards butt up against the controls below). It
// fires only when the list is EMPTY (the reserved rows leave room): a subtle centered hint (a
// folder glyph + one line) instead of a blank box; purely decorative — the actions live below.
{
const float remainY = ImGui::GetContentRegionAvail().y;
if (remainY > walRowH * 1.6f) {

View File

@@ -368,7 +368,9 @@ private:
// ---- Below the info card: verify / stop-mining note + install button (centered, text-fit) ----
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x);
Type().textColored(TypeStyle::Caption, (mining || downgrade) ? Warning() : OnSurfaceMedium(), noteStr);
ImGui::PopTextWrapPos();
ImGui::Spacing();
const float bw = ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x * 2.0f + Layout::spacingLg();
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - bw) * 0.5f));

View File

@@ -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";
@@ -2242,6 +2242,7 @@ void I18n::loadBuiltinEnglish()
strings_["validate_not_mine"] = "Not owned by this wallet";
strings_["validate_ownership"] = "Ownership:";
strings_["validate_results"] = "Results:";
strings_["validate_results_placeholder"] = "Results will appear here";
strings_["validate_shielded_type"] = "Shielded (z-address)";
strings_["validate_status"] = "Status:";
strings_["validate_title"] = "Validate Address";