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>
This commit is contained in:
@@ -1672,6 +1672,7 @@
|
|||||||
"validate_not_mine": "Nicht im Besitz dieser Wallet",
|
"validate_not_mine": "Nicht im Besitz dieser Wallet",
|
||||||
"validate_ownership": "Eigentum:",
|
"validate_ownership": "Eigentum:",
|
||||||
"validate_results": "Ergebnisse:",
|
"validate_results": "Ergebnisse:",
|
||||||
|
"validate_results_placeholder": "Ergebnisse erscheinen hier",
|
||||||
"validate_shielded_type": "Abgeschirmt (z-Adresse)",
|
"validate_shielded_type": "Abgeschirmt (z-Adresse)",
|
||||||
"validate_status": "Status:",
|
"validate_status": "Status:",
|
||||||
"validate_title": "Adresse validieren",
|
"validate_title": "Adresse validieren",
|
||||||
|
|||||||
@@ -1672,6 +1672,7 @@
|
|||||||
"validate_not_mine": "No es propiedad de esta cartera",
|
"validate_not_mine": "No es propiedad de esta cartera",
|
||||||
"validate_ownership": "Propiedad:",
|
"validate_ownership": "Propiedad:",
|
||||||
"validate_results": "Resultados:",
|
"validate_results": "Resultados:",
|
||||||
|
"validate_results_placeholder": "Los resultados aparecerán aquí",
|
||||||
"validate_shielded_type": "Protegida (dirección z)",
|
"validate_shielded_type": "Protegida (dirección z)",
|
||||||
"validate_status": "Estado:",
|
"validate_status": "Estado:",
|
||||||
"validate_title": "Validar Dirección",
|
"validate_title": "Validar Dirección",
|
||||||
|
|||||||
@@ -1672,6 +1672,7 @@
|
|||||||
"validate_not_mine": "N'appartient pas à ce portefeuille",
|
"validate_not_mine": "N'appartient pas à ce portefeuille",
|
||||||
"validate_ownership": "Propriété :",
|
"validate_ownership": "Propriété :",
|
||||||
"validate_results": "Résultats :",
|
"validate_results": "Résultats :",
|
||||||
|
"validate_results_placeholder": "Les résultats apparaîtront ici",
|
||||||
"validate_shielded_type": "Blindée (z-adresse)",
|
"validate_shielded_type": "Blindée (z-adresse)",
|
||||||
"validate_status": "Statut :",
|
"validate_status": "Statut :",
|
||||||
"validate_title": "Valider l'adresse",
|
"validate_title": "Valider l'adresse",
|
||||||
|
|||||||
@@ -1669,6 +1669,7 @@
|
|||||||
"validate_not_mine": "このウォレットに属していません",
|
"validate_not_mine": "このウォレットに属していません",
|
||||||
"validate_ownership": "所有者:",
|
"validate_ownership": "所有者:",
|
||||||
"validate_results": "結果:",
|
"validate_results": "結果:",
|
||||||
|
"validate_results_placeholder": "ここに結果が表示されます",
|
||||||
"validate_shielded_type": "シールド(zアドレス)",
|
"validate_shielded_type": "シールド(zアドレス)",
|
||||||
"validate_status": "ステータス:",
|
"validate_status": "ステータス:",
|
||||||
"validate_title": "アドレスを検証",
|
"validate_title": "アドレスを検証",
|
||||||
|
|||||||
@@ -1671,6 +1671,7 @@
|
|||||||
"validate_not_mine": "이 지갑에 속하지 않음",
|
"validate_not_mine": "이 지갑에 속하지 않음",
|
||||||
"validate_ownership": "소유자:",
|
"validate_ownership": "소유자:",
|
||||||
"validate_results": "결과:",
|
"validate_results": "결과:",
|
||||||
|
"validate_results_placeholder": "결과가 여기에 표시됩니다",
|
||||||
"validate_shielded_type": "차폐 (z 주소)",
|
"validate_shielded_type": "차폐 (z 주소)",
|
||||||
"validate_status": "상태:",
|
"validate_status": "상태:",
|
||||||
"validate_title": "주소 검증",
|
"validate_title": "주소 검증",
|
||||||
|
|||||||
@@ -1672,6 +1672,7 @@
|
|||||||
"validate_not_mine": "Não pertence a esta carteira",
|
"validate_not_mine": "Não pertence a esta carteira",
|
||||||
"validate_ownership": "Propriedade:",
|
"validate_ownership": "Propriedade:",
|
||||||
"validate_results": "Resultados:",
|
"validate_results": "Resultados:",
|
||||||
|
"validate_results_placeholder": "Os resultados aparecerão aqui",
|
||||||
"validate_shielded_type": "Blindado (z-endereço)",
|
"validate_shielded_type": "Blindado (z-endereço)",
|
||||||
"validate_status": "Status:",
|
"validate_status": "Status:",
|
||||||
"validate_title": "Validar Endereço",
|
"validate_title": "Validar Endereço",
|
||||||
|
|||||||
@@ -1672,6 +1672,7 @@
|
|||||||
"validate_not_mine": "Не принадлежит этому кошельку",
|
"validate_not_mine": "Не принадлежит этому кошельку",
|
||||||
"validate_ownership": "Принадлежность:",
|
"validate_ownership": "Принадлежность:",
|
||||||
"validate_results": "Результаты:",
|
"validate_results": "Результаты:",
|
||||||
|
"validate_results_placeholder": "Результаты появятся здесь",
|
||||||
"validate_shielded_type": "Экранированный (z-адрес)",
|
"validate_shielded_type": "Экранированный (z-адрес)",
|
||||||
"validate_status": "Статус:",
|
"validate_status": "Статус:",
|
||||||
"validate_title": "Проверить адрес",
|
"validate_title": "Проверить адрес",
|
||||||
|
|||||||
@@ -1670,6 +1670,7 @@
|
|||||||
"validate_not_mine": "不属于此钱包",
|
"validate_not_mine": "不属于此钱包",
|
||||||
"validate_ownership": "所有权:",
|
"validate_ownership": "所有权:",
|
||||||
"validate_results": "结果:",
|
"validate_results": "结果:",
|
||||||
|
"validate_results_placeholder": "结果将显示在此处",
|
||||||
"validate_shielded_type": "屏蔽(z 地址)",
|
"validate_shielded_type": "屏蔽(z 地址)",
|
||||||
"validate_status": "状态:",
|
"validate_status": "状态:",
|
||||||
"validate_title": "验证地址",
|
"validate_title": "验证地址",
|
||||||
|
|||||||
@@ -4174,7 +4174,12 @@ void App::renderSeedMigrationDialog()
|
|||||||
case SeedMigrationStep::ShowSeed: {
|
case SeedMigrationStep::ShowSeed: {
|
||||||
ui::material::DialogWarningHeader(TR("mig_seed_warning"));
|
ui::material::DialogWarningHeader(TR("mig_seed_warning"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ui::RenderSeedWordGrid(ui::SplitSeedWords(seed_migration_seed_));
|
// 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::Spacing();
|
||||||
ImGui::TextColored(kMedium, "%s", TR("mig_receive_addr"));
|
ImGui::TextColored(kMedium, "%s", TR("mig_receive_addr"));
|
||||||
ImGui::TextWrapped("%s", seed_migration_dest_.c_str());
|
ImGui::TextWrapped("%s", seed_migration_dest_.c_str());
|
||||||
|
|||||||
@@ -1450,6 +1450,11 @@ void App::renderEncryptWalletDialog() {
|
|||||||
ov.cardWidth = 460.0f; ov.idSuffix = "changepass";
|
ov.cardWidth = 460.0f; ov.idSuffix = "changepass";
|
||||||
if (BeginOverlayDialog(ov)) {
|
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::TextUnformatted(TR("change_pass_current"));
|
||||||
ImGui::PushItemWidth(-1);
|
ImGui::PushItemWidth(-1);
|
||||||
ImGui::InputText("##chg_old", change_old_pass_buf_, sizeof(change_old_pass_buf_),
|
ImGui::InputText("##chg_old", change_old_pass_buf_, sizeof(change_old_pass_buf_),
|
||||||
|
|||||||
@@ -294,29 +294,43 @@ void App::renderFirstRunWizard() {
|
|||||||
{
|
{
|
||||||
int state = cardState(0);
|
int state = cardState(0);
|
||||||
bool isFocused = (state == 1);
|
bool isFocused = (state == 1);
|
||||||
|
bool isCollapsed = (state == 2); // Completed: minimize to a compact pill (mirrors Card 1)
|
||||||
float cx = leftX + cardPad;
|
float cx = leftX + cardPad;
|
||||||
float cy = card0Top + cardPad;
|
float cy = card0Top + cardPad;
|
||||||
float contentW = colW - 2 * cardPad;
|
float contentW = colW - 2 * cardPad;
|
||||||
|
|
||||||
// Step indicator
|
// 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;
|
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
|
||||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
||||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, TR("wiz_step1"));
|
float labelX = cx + iconW + 4.0f * dp;
|
||||||
cy += captionFont->LegacySize + 6.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;
|
||||||
|
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
||||||
|
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, TR("wiz_step1"));
|
||||||
|
cy += captionFont->LegacySize + 6.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
{
|
{
|
||||||
const char* t = TR("wiz_appearance");
|
const char* t = TR("wiz_appearance");
|
||||||
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
|
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
|
||||||
cy += titleFont->LegacySize + 10.0f * dp;
|
cy += titleFont->LegacySize + 10.0f * dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separator
|
// Separator
|
||||||
dl->AddLine(ImVec2(cx, cy), ImVec2(cx + contentW, cy),
|
dl->AddLine(ImVec2(cx, cy), ImVec2(cx + contentW, cy),
|
||||||
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
|
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
|
||||||
cy += 14.0f * dp;
|
cy += 14.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
float& wiz_blur_amount = wizardUi.blur_amount;
|
float& wiz_blur_amount = wizardUi.blur_amount;
|
||||||
bool& wiz_theme_effects = wizardUi.theme_effects;
|
bool& wiz_theme_effects = wizardUi.theme_effects;
|
||||||
@@ -352,6 +366,9 @@ void App::renderFirstRunWizard() {
|
|||||||
wiz_appearance_init = true;
|
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
|
// Render controls always so content is visible under the dim
|
||||||
// overlay when not focused; disable interaction when not active.
|
// overlay when not focused; disable interaction when not active.
|
||||||
ImGui::BeginDisabled(!isFocused);
|
ImGui::BeginDisabled(!isFocused);
|
||||||
@@ -668,13 +685,21 @@ void App::renderFirstRunWizard() {
|
|||||||
cy += btnH;
|
cy += btnH;
|
||||||
}
|
}
|
||||||
|
|
||||||
cy += cardPad;
|
} // if (!isCollapsed)
|
||||||
// Lock card height to the tallest content ever seen
|
|
||||||
float& card0MaxH = wizardUi.card0_max_h;
|
|
||||||
card0MaxH = std::max(card0MaxH, cy - card0Top);
|
|
||||||
card0Bot = card0Top + card0MaxH;
|
|
||||||
|
|
||||||
// Card 0 finalization deferred until after cards 1+2 are sized
|
cy += cardPad;
|
||||||
|
// 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1447,7 +1472,9 @@ void App::renderFirstRunWizard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Deferred Card 0 finalization: match right column total height ---
|
// --- 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;
|
float rightColBot = card2Bot;
|
||||||
if (rightColBot > card0Bot) card0Bot = rightColBot;
|
if (rightColBot > card0Bot) card0Bot = rightColBot;
|
||||||
finalizeCard(leftX, colW, card0Top, card0Bot, cardState(0));
|
finalizeCard(leftX, colW, card0Top, card0Bot, cardState(0));
|
||||||
|
|||||||
@@ -1640,7 +1640,10 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
|||||||
// the measuring frame (its geometry is a placeholder; the whole card is hidden until centered).
|
// the measuring frame (its geometry is a placeholder; the whole card is hidden until centered).
|
||||||
if (!floating && !hideForMeasure) {
|
if (!floating && !hideForMeasure) {
|
||||||
GlassPanelSpec cardGlass;
|
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);
|
DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -169,9 +169,9 @@ public:
|
|||||||
}
|
}
|
||||||
if (amountValid && newFromBal < 1e-9) {
|
if (amountValid && newFromBal < 1e-9) {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
// Full-balance send: same warning-icon treatment as the de-shielding header above,
|
||||||
ImGui::TextWrapped("%s", TR("sends_full_balance_warning"));
|
// so this stakes-bearing line reads as distinct from the neutral preview text.
|
||||||
ImGui::PopStyleColor();
|
DialogWarningHeader(TR("sends_full_balance_warning"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
|
|||||||
@@ -207,6 +207,28 @@ static bool isShieldedAddr(const std::string& a) {
|
|||||||
return !a.empty() && a[0] == 'z';
|
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
|
// 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.
|
// 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) {
|
static ImU32 contactTypeColor(bool shielded, bool light) {
|
||||||
@@ -1219,10 +1241,13 @@ void RenderContactsTab(App* app)
|
|||||||
dl->AddText(lblF, lblSz, ImVec2(tx, ty), material::OnSurface(), entry.label.c_str());
|
dl->AddText(lblF, lblSz, ImVec2(tx, ty), material::OnSurface(), entry.label.c_str());
|
||||||
dl->PopClipRect();
|
dl->PopClipRect();
|
||||||
// Un-collapse to the full address on hover (clipped to the text column so it never
|
// 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
|
std::string addr = rowHovered
|
||||||
? entry.address
|
? 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->PushClipRect(ImVec2(tx, mn.y), ImVec2(textMaxX, mx.y), true);
|
||||||
dl->AddText(adrF, adrSz, ImVec2(tx, ty + lblSz + 3.0f * dp),
|
dl->AddText(adrF, adrSz, ImVec2(tx, ty + lblSz + 3.0f * dp),
|
||||||
material::OnSurfaceMedium(), addr.c_str());
|
material::OnSurfaceMedium(), addr.c_str());
|
||||||
|
|||||||
@@ -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"));
|
ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("explorer_chain_stats"));
|
||||||
drawStatusPill(cardMin, cardW);
|
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,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(cardMin.x + pad, labelY), OnSurfaceMedium(), TR("explorer_block_height"));
|
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);
|
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,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(cardMin.x + pad, hashLabelY), OnSurfaceMedium(), TR("peers_best_block"));
|
ImVec2(cardMin.x + pad, hashLabelY), OnSurfaceMedium(), TR("peers_best_block"));
|
||||||
|
|
||||||
|
|||||||
@@ -2136,11 +2136,29 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
|||||||
float rowGap = pfRowGapFor(style, mktDp);
|
float rowGap = pfRowGapFor(style, mktDp);
|
||||||
float rowsH = std::max(rowH, portfolioH - pfSummaryH);
|
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));
|
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
|
// 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::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||||
ImGui::BeginChild("##pfRows", ImVec2(availWidth, rowsH), false);
|
ImGui::BeginChild("##pfRows", ImVec2(availWidth, panelH), false, ImGuiWindowFlags_NoBackground);
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
// Zero item-spacing INSIDE the child: each row emits an InvisibleButton + a gap Dummy, and
|
// 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
|
// ImGui's default ItemSpacing.y between those items would inflate the content past rowsH and
|
||||||
|
|||||||
@@ -362,9 +362,14 @@ static void RenderRecentReceived(const AddressInfo& /* addr */,
|
|||||||
if (tx.type != "receive" && tx.type != "mined") continue;
|
if (tx.type != "receive" && tx.type != "mined") continue;
|
||||||
recvs.push_back(&tx);
|
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);
|
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,
|
ImGui::BeginChild("##RecentReceivedRows", ImVec2(width, listH), false,
|
||||||
ImGuiWindowFlags_NoBackground);
|
ImGuiWindowFlags_NoBackground);
|
||||||
@@ -372,8 +377,9 @@ static void RenderRecentReceived(const AddressInfo& /* addr */,
|
|||||||
|
|
||||||
char buf[64];
|
char buf[64];
|
||||||
if (recvs.empty()) {
|
if (recvs.empty()) {
|
||||||
ImGui::SetCursorPosY(Layout::spacingMd());
|
// Fill the empty canvas with a centered material empty-state (icon + title)
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_recent_receives"));
|
// 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();
|
ImGui::EndChild();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1123,9 +1123,14 @@ static void RenderRecentSends(const WalletState& state, float width, ImFont* cap
|
|||||||
if (tx.type != "send" && tx.type != "shield") continue;
|
if (tx.type != "send" && tx.type != "shield") continue;
|
||||||
sends.push_back(&tx);
|
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);
|
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,
|
ImGui::BeginChild("##RecentSendRows", ImVec2(width, listH), false,
|
||||||
ImGuiWindowFlags_NoBackground);
|
ImGuiWindowFlags_NoBackground);
|
||||||
@@ -1133,8 +1138,9 @@ static void RenderRecentSends(const WalletState& state, float width, ImFont* cap
|
|||||||
|
|
||||||
char buf[64];
|
char buf[64];
|
||||||
if (sends.empty()) {
|
if (sends.empty()) {
|
||||||
ImGui::SetCursorPosY(Layout::spacingMd());
|
// Fill the empty canvas with a centered material empty-state (icon + title)
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("send_no_recent"));
|
// 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();
|
ImGui::EndChild();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,7 +146,10 @@ void ShieldDialog::render(App* app)
|
|||||||
|
|
||||||
ImGui::Spacing();
|
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::Text("%s", TR("fee_label"));
|
||||||
ImGui::SetNextItemWidth(feeInput.width * Layout::dpiScale());
|
ImGui::SetNextItemWidth(feeInput.width * Layout::dpiScale());
|
||||||
ImGui::InputDouble("##Fee", &s_fee, 0.0001, 0.001, "%.8f");
|
ImGui::InputDouble("##Fee", &s_fee, 0.0001, 0.001, "%.8f");
|
||||||
@@ -155,16 +158,18 @@ void ShieldDialog::render(App* app)
|
|||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextDisabled("DRGX");
|
ImGui::TextDisabled("DRGX");
|
||||||
|
|
||||||
ImGui::Spacing();
|
// 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".
|
||||||
// UTXO limit
|
ImGui::SameLine(pairColX);
|
||||||
|
ImGui::BeginGroup();
|
||||||
ImGui::Text("%s", TR("shield_utxo_limit"));
|
ImGui::Text("%s", TR("shield_utxo_limit"));
|
||||||
ImGui::SetNextItemWidth(utxoInput.width * Layout::dpiScale());
|
ImGui::SetNextItemWidth(utxoInput.width * Layout::dpiScale());
|
||||||
ImGui::InputInt("##Limit", &s_utxo_limit);
|
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 < 1) s_utxo_limit = 1;
|
||||||
if (s_utxo_limit > 100) s_utxo_limit = 100;
|
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();
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
|||||||
@@ -208,6 +208,14 @@ void ValidateAddressDialog::render(App* app)
|
|||||||
}
|
}
|
||||||
} else if (!app->isConnected()) {
|
} else if (!app->isConnected()) {
|
||||||
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"));
|
||||||
|
} 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"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close button at bottom — centered via the shared footer helper (no separator, matching
|
// Close button at bottom — centered via the shared footer helper (no separator, matching
|
||||||
|
|||||||
@@ -71,9 +71,12 @@ public:
|
|||||||
const int kMaxVisibleRows = 7;
|
const int kMaxVisibleRows = 7;
|
||||||
ImFont* nameFont = Type().subtitle1();
|
ImFont* nameFont = Type().subtitle1();
|
||||||
ImFont* metaFont = Type().caption();
|
ImFont* metaFont = Type().caption();
|
||||||
// The list is always sized to its MAX height (kMaxVisibleRows) for a consistent modal size — it
|
// Size the list to the ACTUAL wallet count so a few wallets sit tight against the controls below
|
||||||
// does not shrink to fit a few wallets; fewer rows leave empty space, more than 7 scroll.
|
// instead of stranding ~200px of empty rows before the create/scan prompts; cap at kMaxVisibleRows
|
||||||
const int visRows = 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 cardPadY = Layout::spacingMd(); // roomier cards (was spacingSm)
|
||||||
const float walRowH = cardPadY * 2.0f + nameFont->LegacySize + Layout::spacingSm() + metaFont->LegacySize;
|
const float walRowH = cardPadY * 2.0f + nameFont->LegacySize + Layout::spacingSm() + metaFont->LegacySize;
|
||||||
const float cardGap = Layout::spacingMd(); // more breathing room between wallet cards
|
const float cardGap = Layout::spacingMd(); // more breathing room between wallet cards
|
||||||
@@ -421,9 +424,10 @@ public:
|
|||||||
ImGui::Dummy(ImVec2(rowW, walRowH));
|
ImGui::Dummy(ImVec2(rowW, walRowH));
|
||||||
if (i + 1 < s_rows.size()) ImGui::Dummy(ImVec2(rowW, cardGap));
|
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
|
// Empty-state nudge: the list is now sized to the actual wallet count, so a populated list has
|
||||||
// space below. When there's real room to spare, fill it with a subtle centered hint (a folder
|
// no room to spare and this draws nothing (the cards butt up against the controls below). It
|
||||||
// glyph + one line) instead of dead space; it's purely decorative — the actions live below.
|
// 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;
|
const float remainY = ImGui::GetContentRegionAvail().y;
|
||||||
if (remainY > walRowH * 1.6f) {
|
if (remainY > walRowH * 1.6f) {
|
||||||
|
|||||||
@@ -2242,6 +2242,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["validate_not_mine"] = "Not owned by this wallet";
|
strings_["validate_not_mine"] = "Not owned by this wallet";
|
||||||
strings_["validate_ownership"] = "Ownership:";
|
strings_["validate_ownership"] = "Ownership:";
|
||||||
strings_["validate_results"] = "Results:";
|
strings_["validate_results"] = "Results:";
|
||||||
|
strings_["validate_results_placeholder"] = "Results will appear here";
|
||||||
strings_["validate_shielded_type"] = "Shielded (z-address)";
|
strings_["validate_shielded_type"] = "Shielded (z-address)";
|
||||||
strings_["validate_status"] = "Status:";
|
strings_["validate_status"] = "Status:";
|
||||||
strings_["validate_title"] = "Validate Address";
|
strings_["validate_title"] = "Validate Address";
|
||||||
|
|||||||
Reference in New Issue
Block a user