fix(ui): scale unscaled geometry at HiDPI — inputs, send progress cards, chat/pool overlaps, peer rows

At font_scale 1.5 ~13 sites read absolute geometry (schema .size/.width) straight
into ImGui without ×dpiScale(), so they stayed native-size while the font grew and
overlapped/clipped real text or money:

- Shield/Merge fee + UTXO inputs, Request Payment amount, Block Info height input:
  ×dpiScale() so the value no longer clips (e.g. 0.00010000 -> 0.00010).
- Send: the confirm-popup Amount Details divider (floored the row step at the scaled
  caption height so it no longer strikes the Fee row), the tx-progress error and
  sending/success cards, and the zero-balance CTA button — all ×dp.
- Mining pool row: ellipsis-truncate the hostname so it can't collide with the
  right-aligned hashrate.
- Chat conversation list: scale the pane-width clamp AND clip the peer name to the
  column left of the timestamp (measure-then-clip) so name and time never overlap.
- Explorer block-detail label column, Peers row offsets, and DialogConfirmFooter
  button height — ×dp.

transaction_details keeps its negative fill-sentinel widths unscaled (a content
margin, not raw px). Verified at font_scale 1.5 across full-node + Lite + Windows
(ctest green) and an adversarial diff review (three wrong-scale regressions fixed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-18 20:53:29 -05:00
parent 755cf22ad0
commit 99d1e73676
10 changed files with 84 additions and 55 deletions

View File

@@ -1737,7 +1737,7 @@ inline void DialogWarningHeader(const char* warningLabel, const ImVec4& col = Wa
inline void DialogConfirmFooter(const char* cancelId, const char* confirmLabel, inline void DialogConfirmFooter(const char* cancelId, const char* confirmLabel,
bool danger, bool& outCancel, bool& outConfirm) bool danger, bool& outCancel, bool& outConfirm)
{ {
float btnH = schema::UI().drawElement("components.overlay-dialog", "confirm-btn-height").sizeOr(40.0f); float btnH = schema::UI().drawElement("components.overlay-dialog", "confirm-btn-height").sizeOr(40.0f) * Layout::dpiScale();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button(cancelId, ImVec2(btnW, btnH))) { if (ImGui::Button(cancelId, ImVec2(btnW, btnH))) {
outCancel = true; outCancel = true;

View File

@@ -112,7 +112,7 @@ void BlockInfoDialog::render(App* app)
// Height input // Height input
ImGui::Text("%s", TR("block_height")); ImGui::Text("%s", TR("block_height"));
ImGui::SetNextItemWidth(heightInput.width); ImGui::SetNextItemWidth(heightInput.width * Layout::dpiScale());
ImGui::InputInt("##Height", &s_height); ImGui::InputInt("##Height", &s_height);
if (s_height < 1) s_height = 1; if (s_height < 1) s_height = 1;
// Clamp to the chain tip so navigation/typing can't request a height // Clamp to the chain tip so navigation/typing can't request a height

View File

@@ -776,7 +776,7 @@ void RenderChatTab(App* app)
} }
const ImVec2 avail = ImGui::GetContentRegionAvail(); const ImVec2 avail = ImGui::GetContentRegionAvail();
const float listW = std::clamp(avail.x * 0.32f, 220.0f, 360.0f); const float listW = std::clamp(avail.x * 0.32f, 220.0f * Layout::dpiScale(), 360.0f * Layout::dpiScale());
// Row geometry is logical px — scale by dpiScale() so rows/padding grow with the (DPI-scaled) // Row geometry is logical px — scale by dpiScale() so rows/padding grow with the (DPI-scaled)
// fonts. Left raw, at higher DPI the row was too short for the enlarged text and the preview's // fonts. Left raw, at higher DPI the row was too short for the enlarged text and the preview's
// right margin (rowW - pad) shrank to ~zero, clipping the last glyph mid-word. // right margin (rowW - pad) shrank to ~zero, clipping the last glyph mid-word.
@@ -895,16 +895,22 @@ void RenderChatTab(App* app)
} }
const float textX = avC.x + avR + pad; const float textX = avC.x + avR + pad;
// Name (top). Hidden conversations (shown via "Show hidden") render dimmed. // Time (top-right, muted) — compact relative form (Q5). Measure/draw it FIRST so the name can be
dl->AddText(nameFont, nameSz, ImVec2(textX, p.y + pad), // clipped to the column left of it — otherwise a long peer name overruns the timestamp (worse at
c.hidden ? material::OnSurfaceMedium() : material::OnSurface(), c.peerName.c_str()); // HiDPI, where the fixed-length name grows ~1.5x).
// Time (top-right, muted) — compact relative form (Q5).
const std::string when = relativeTime(c.lastTs); const std::string when = relativeTime(c.lastTs);
float nameRight = mx.x - pad;
if (!when.empty()) { if (!when.empty()) {
const ImVec2 wsz = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, when.c_str()); const ImVec2 wsz = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, when.c_str());
dl->AddText(metaFont, metaSz, ImVec2(mx.x - pad - wsz.x, p.y + pad + 1.0f), dl->AddText(metaFont, metaSz, ImVec2(mx.x - pad - wsz.x, p.y + pad + 1.0f),
material::OnSurfaceMedium(), when.c_str()); material::OnSurfaceMedium(), when.c_str());
nameRight = mx.x - pad - wsz.x - pad; // reserve the timestamp column + a gap
} }
// Name (top), clipped to the space left of the timestamp. Hidden conversations render dimmed.
dl->PushClipRect(ImVec2(textX, p.y), ImVec2(std::max(textX, nameRight), mx.y), true);
dl->AddText(nameFont, nameSz, ImVec2(textX, p.y + pad),
c.hidden ? material::OnSurfaceMedium() : material::OnSurface(), c.peerName.c_str());
dl->PopClipRect();
// Preview (bottom, clipped to the text column, muted). // Preview (bottom, clipped to the text column, muted).
const std::string preview = previewOf(c.lastBody); const std::string preview = previewOf(c.lastBody);
dl->PushClipRect(ImVec2(textX, p.y), ImVec2(mx.x - pad, mx.y), true); dl->PushClipRect(ImVec2(textX, p.y), ImVec2(mx.x - pad, mx.y), true);

View File

@@ -1116,8 +1116,9 @@ static void renderBlockDetailModal(App* app) {
// ── Info grid ── // ── Info grid ──
ImDrawList* dl = ImGui::GetWindowDrawList(); ImDrawList* dl = ImGui::GetWindowDrawList();
float dp = Layout::dpiScale();
float rowH = capFont->LegacySize + Layout::spacingXs() + sub1->LegacySize; float rowH = capFont->LegacySize + Layout::spacingXs() + sub1->LegacySize;
float labelW = S.drawElement("tabs.explorer", "label-column").size; float labelW = S.drawElement("tabs.explorer", "label-column").size * dp;
{ {
ImVec2 gridPos = ImGui::GetCursorScreenPos(); ImVec2 gridPos = ImGui::GetCursorScreenPos();
float gx = gridPos.x; float gx = gridPos.x;

View File

@@ -312,11 +312,11 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
isCurrent ? Success() : OnSurfaceDisabled()); isCurrent ? Success() : OnSurfaceDisabled());
const float textY = rMin.y + (listRowH - capFont->LegacySize) * 0.5f; const float textY = rMin.y + (listRowH - capFont->LegacySize) * 0.5f;
// Show the short host label (the narrow card can't fit host:port); the
// full stratum URL is in the hover tooltip.
cdl->AddText(capFont, capFont->LegacySize, ImVec2(rMin.x + 16 * dp, textY),
isCurrent ? Primary() : OnSurface(), kp.label.c_str());
// Build the right-side "<hashrate> N% fee" run first so we know its width
// and can bound (and ellipsis-truncate) the left host label to avoid a
// collision — both text runs grow ~1.5x at font_scale 1.5 while the card
// width barely does.
char right[64]; char right[64];
std::string hrStr = haveHr ? FormatHashrate(it->second.hashrateHs) : std::string(""); std::string hrStr = haveHr ? FormatHashrate(it->second.hashrateHs) : std::string("");
// Prefer the live fee the pool reports; fall back to the compile-time // Prefer the live fee the pool reports; fall back to the compile-time
@@ -331,6 +331,16 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
else else
snprintf(right, sizeof(right), "%s", hrStr.c_str()); snprintf(right, sizeof(right), "%s", hrStr.c_str());
ImVec2 rSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, right); ImVec2 rSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, right);
// Show the short host label (the narrow card can't fit host:port); the
// full stratum URL is in the hover tooltip. Truncate it to the gap left
// of the right-side run so a long hostname can't overlap the hashrate.
const float labelX = rMin.x + 16 * dp;
const float labelMaxW = (rMax.x - rSz.x - 6 * dp - gap) - labelX;
std::string label = TruncateToWidth(kp.label, capFont, capFont->LegacySize, labelMaxW);
cdl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, textY),
isCurrent ? Primary() : OnSurface(), label.c_str());
cdl->AddText(capFont, capFont->LegacySize, cdl->AddText(capFont, capFont->LegacySize,
ImVec2(rMax.x - rSz.x - 6 * dp, textY), OnSurfaceMedium(), right); ImVec2(rMax.x - rSz.x - 6 * dp, textY), OnSurfaceMedium(), right);

View File

@@ -627,6 +627,7 @@ void RenderPeersTab(App* app)
ImGui::Dummy(ImVec2(0, 20)); ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("peers_no_connected")); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("peers_no_connected"));
} else { } else {
const float dp = ui::Layout::dpiScale();
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg(); float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg();
float rowInset = Layout::spacingLg(); float rowInset = Layout::spacingLg();
float innerW = ImGui::GetContentRegionAvail().x - rowInset * 2; float innerW = ImGui::GetContentRegionAvail().x - rowInset * 2;
@@ -643,13 +644,13 @@ void RenderPeersTab(App* app)
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH); ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
if (is_selected) { if (is_selected) {
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), S.drawElement("tabs.peers", "row-selection-rounding").size); dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), S.drawElement("tabs.peers", "row-selection-rounding").size * dp);
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + S.drawElement("tabs.peers", "row-accent-width").size, rowEnd.y), Primary(), S.drawElement("tabs.peers", "row-accent-rounding").size); dl->AddRectFilled(rowPos, ImVec2(rowPos.x + S.drawElement("tabs.peers", "row-accent-width").size * dp, rowEnd.y), Primary(), S.drawElement("tabs.peers", "row-accent-rounding").size * dp);
} }
bool hovered = material::IsRectHovered(rowPos, rowEnd); bool hovered = material::IsRectHovered(rowPos, rowEnd);
if (hovered && !is_selected) { if (hovered && !is_selected) {
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.peers", "row-selection-rounding").size); dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.peers", "row-selection-rounding").size * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
} }
@@ -662,9 +663,9 @@ void RenderPeersTab(App* app)
else if (ping_ms < 500) dotCol = Warning(); else if (ping_ms < 500) dotCol = Warning();
else dotCol = Error(); else dotCol = Error();
float pingDotR = S.drawElement("tabs.peers", "ping-dot-radius-base").size + S.drawElement("tabs.peers", "ping-dot-radius-scale").size * hs; float pingDotR = S.drawElement("tabs.peers", "ping-dot-radius-base").size + S.drawElement("tabs.peers", "ping-dot-radius-scale").size * hs;
dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ping-dot-x-offset").size, cy + body2->LegacySize * 0.5f), pingDotR, dotCol); dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ping-dot-x-offset").size * dp, cy + body2->LegacySize * 0.5f), pingDotR, dotCol);
float addrX = cx + S.drawElement("tabs.peers", "address-x-offset").size; float addrX = cx + S.drawElement("tabs.peers", "address-x-offset").size * dp;
// Reserve the line-1 trailing zone (ping + direction pill live on the far right, // Reserve the line-1 trailing zone (ping + direction pill live on the far right,
// the nearest being the ping at innerW - pingW - spacingXl*3). A long IPv6+port // the nearest being the ping at innerW - pingW - spacingXl*3). A long IPv6+port
// addr must clip before it so it can't run under those. Mirror the ping formula // addr must clip before it so it can't run under those. Mirror the ping formula
@@ -693,9 +694,9 @@ void RenderPeersTab(App* app)
ImU32 dirFg = peer.inbound ? WithAlpha(Success(), 200) : WithAlpha(Secondary(), 200); ImU32 dirFg = peer.inbound ? WithAlpha(Success(), 200) : WithAlpha(Secondary(), 200);
ImVec2 dirSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, dirLabel); ImVec2 dirSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, dirLabel);
float dirX = rowPos.x + innerW - dirSz.x - Layout::spacingXl(); float dirX = rowPos.x + innerW - dirSz.x - Layout::spacingXl();
ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size); ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size * dp);
ImVec2 pillMax(dirX + dirSz.x + S.drawElement("tabs.peers", "dir-pill-padding").size, cy + capFont->LegacySize + S.drawElement("tabs.peers", "dir-pill-y-bottom").size); ImVec2 pillMax(dirX + dirSz.x + S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + capFont->LegacySize + S.drawElement("tabs.peers", "dir-pill-y-bottom").size * dp);
dl->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size); dl->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size * dp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2), dirFg, dirLabel); dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2), dirFg, dirLabel);
} }
@@ -708,7 +709,7 @@ void RenderPeersTab(App* app)
} }
float cy2 = cy + body2->LegacySize + Layout::spacingXs(); float cy2 = cy + body2->LegacySize + Layout::spacingXs();
float subverX = cx + S.drawElement("tabs.peers", "address-x-offset").size; float subverX = cx + S.drawElement("tabs.peers", "address-x-offset").size * dp;
// Reserve the line-2 trailing widths up front so an untrusted, arbitrarily long // Reserve the line-2 trailing widths up front so an untrusted, arbitrarily long
// remote subver cannot push the TLS badge / ban-score off the row. Order right→left: // remote subver cannot push the TLS badge / ban-score off the row. Order right→left:
@@ -747,7 +748,7 @@ void RenderPeersTab(App* app)
ImU32 tlsFg = WithAlpha(Success(), 200); ImU32 tlsFg = WithAlpha(Success(), 200);
ImVec2 tlsMin(badgeX, cy2); ImVec2 tlsMin(badgeX, cy2);
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2); ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2);
dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size); dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size * dp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4, cy2 + 1), tlsFg, "TLS"); dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4, cy2 + 1), tlsFg, "TLS");
} else { } else {
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2), dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2),
@@ -815,7 +816,7 @@ void RenderPeersTab(App* app)
if (i < state.peers.size() - 1) { if (i < state.peers.size() - 1) {
ImVec2 divStart = ImGui::GetCursorScreenPos(); ImVec2 divStart = ImGui::GetCursorScreenPos();
dl->AddLine(ImVec2(divStart.x + pad + 18, divStart.y), dl->AddLine(ImVec2(divStart.x + pad + 18 * dp, divStart.y),
ImVec2(divStart.x + innerW - pad, divStart.y), ImVec2(divStart.x + innerW - pad, divStart.y),
IM_COL32(255, 255, 255, 15)); IM_COL32(255, 255, 255, 15));
} }
@@ -832,7 +833,8 @@ void RenderPeersTab(App* app)
ImGui::Dummy(ImVec2(0, 20)); ImGui::Dummy(ImVec2(0, 20));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("peers_no_banned")); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("peers_no_banned"));
} else { } else {
float rowH = capFont->LegacySize + S.drawElement("tabs.peers", "banned-row-height-padding").size; const float dp = ui::Layout::dpiScale();
float rowH = capFont->LegacySize + S.drawElement("tabs.peers", "banned-row-height-padding").size * dp;
float rowInsetB = pad; float rowInsetB = pad;
float innerW = ImGui::GetContentRegionAvail().x - rowInsetB * 2; float innerW = ImGui::GetContentRegionAvail().x - rowInsetB * 2;
listScrollY = ImGui::GetScrollY(); listScrollY = ImGui::GetScrollY();
@@ -848,21 +850,21 @@ void RenderPeersTab(App* app)
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH); ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
if (is_selected) { if (is_selected) {
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), S.drawElement("tabs.peers", "banned-row-rounding").size); dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), S.drawElement("tabs.peers", "banned-row-rounding").size * dp);
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + S.drawElement("tabs.peers", "row-accent-width").size, rowEnd.y), WithAlpha(Error(), 200), S.drawElement("tabs.peers", "banned-accent-rounding").size); dl->AddRectFilled(rowPos, ImVec2(rowPos.x + S.drawElement("tabs.peers", "row-accent-width").size * dp, rowEnd.y), WithAlpha(Error(), 200), S.drawElement("tabs.peers", "banned-accent-rounding").size * dp);
} }
if (material::IsRectHovered(rowPos, rowEnd) && !is_selected) { if (material::IsRectHovered(rowPos, rowEnd) && !is_selected) {
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.peers", "banned-row-rounding").size); dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.peers", "banned-row-rounding").size * dp);
} }
float cx = rowPos.x + pad; float cx = rowPos.x + pad;
float cy = rowPos.y + Layout::spacingXs(); float cy = rowPos.y + Layout::spacingXs();
float banDotR = S.drawElement("tabs.peers", "ban-dot-radius-base").size + S.drawElement("tabs.peers", "ban-dot-radius-scale").size * hs; float banDotR = S.drawElement("tabs.peers", "ban-dot-radius-base").size + S.drawElement("tabs.peers", "ban-dot-radius-scale").size * hs;
dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ban-dot-x-offset").size, cy + capFont->LegacySize * 0.4f), banDotR, WithAlpha(Error(), 200)); dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ban-dot-x-offset").size * dp, cy + capFont->LegacySize * 0.4f), banDotR, WithAlpha(Error(), 200));
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "banned-address-x-offset").size, cy), dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "banned-address-x-offset").size * dp, cy),
OnSurfaceDisabled(), banned.address.c_str()); OnSurfaceDisabled(), banned.address.c_str());
std::string banUntil = banned.getBannedUntilString(); std::string banUntil = banned.getBannedUntilString();
@@ -878,7 +880,7 @@ void RenderPeersTab(App* app)
} }
ImGui::SetCursorScreenPos(rowPos); ImGui::SetCursorScreenPos(rowPos);
ImGui::InvisibleButton("##bannedRow", ImVec2(innerW - S.drawElement("tabs.peers", "banned-row-btn-reserve").size, rowH)); ImGui::InvisibleButton("##bannedRow", ImVec2(innerW - S.drawElement("tabs.peers", "banned-row-btn-reserve").size * dp, rowH));
if (ImGui::IsItemClicked(0)) { if (ImGui::IsItemClicked(0)) {
s_selected_banned_idx = static_cast<int>(i); s_selected_banned_idx = static_cast<int>(i);
} }
@@ -898,7 +900,7 @@ void RenderPeersTab(App* app)
if (i < state.bannedPeers.size() - 1) { if (i < state.bannedPeers.size() - 1) {
ImVec2 divStart = ImGui::GetCursorScreenPos(); ImVec2 divStart = ImGui::GetCursorScreenPos();
dl->AddLine(ImVec2(divStart.x + pad + 8, divStart.y), dl->AddLine(ImVec2(divStart.x + pad + 8 * dp, divStart.y),
ImVec2(divStart.x + innerW - pad, divStart.y), ImVec2(divStart.x + innerW - pad, divStart.y),
IM_COL32(255, 255, 255, 15)); IM_COL32(255, 255, 255, 15));
} }

View File

@@ -146,7 +146,7 @@ void RequestPaymentDialog::render(App* app)
// Amount (optional) // Amount (optional)
ImGui::Text("%s", TR("request_amount")); ImGui::Text("%s", TR("request_amount"));
ImGui::SetNextItemWidth(amountInput.width); ImGui::SetNextItemWidth(amountInput.width * Layout::dpiScale());
if (ImGui::InputDouble("##Amount", &s_amount, 0.1, 1.0, "%.8f")) { if (ImGui::InputDouble("##Amount", &s_amount, 0.1, 1.0, "%.8f")) {
s_uri_dirty = true; s_uri_dirty = true;
} }

View File

@@ -539,6 +539,8 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
} }
if (s_tx_status.empty() && !s_sending) return; if (s_tx_status.empty() && !s_sending) return;
const float dp = Layout::dpiScale();
// Drive error styling from the authoritative result flag, not English substrings in a // Drive error styling from the authoritative result flag, not English substrings in a
// (translatable) status string — otherwise a failed send renders as a green success // (translatable) status string — otherwise a failed send renders as a green success
// under a non-English locale. // under a non-English locale.
@@ -547,8 +549,8 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
// ---- ERROR: absolute-positioned overlay, does not displace layout ---- // ---- ERROR: absolute-positioned overlay, does not displace layout ----
if (is_error) { if (is_error) {
float pad = Layout::spacingLg(); float pad = Layout::spacingLg();
float btnH = std::max(schema::UI().drawElement("tabs.send", "error-btn-min-height").size, schema::UI().drawElement("tabs.send", "error-btn-height").size); float btnH = std::max(schema::UI().drawElement("tabs.send", "error-btn-min-height").size, schema::UI().drawElement("tabs.send", "error-btn-height").size) * dp;
float textWrapW = w - pad * 2 - schema::UI().drawElement("tabs.send", "error-icon-inset").size; // icon space float textWrapW = w - pad * 2 - schema::UI().drawElement("tabs.send", "error-icon-inset").size * dp; // icon space
ImVec2 textSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, textWrapW, s_tx_status.c_str()); ImVec2 textSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, textWrapW, s_tx_status.c_str());
float contentH = textSz.y + Layout::spacingMd() + btnH + pad * 2; float contentH = textSz.y + Layout::spacingMd() + btnH + pad * 2;
@@ -572,7 +574,7 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
DrawGlassPanel(fgDl, pMin, pMax, errGlass); DrawGlassPanel(fgDl, pMin, pMax, errGlass);
// Red accent bar on left // Red accent bar on left
fgDl->AddRectFilled(pMin, ImVec2(pMin.x + schema::UI().drawElement("tabs.send", "error-accent-bar-width").size, pMax.y), Error(), errGlass.rounding); fgDl->AddRectFilled(pMin, ImVec2(pMin.x + schema::UI().drawElement("tabs.send", "error-accent-bar-width").size * dp, pMax.y), Error(), errGlass.rounding);
float ix = pMin.x + pad; float ix = pMin.x + pad;
float iy = pMin.y + pad; float iy = pMin.y + pad;
@@ -583,18 +585,18 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
const char* errIcon = ICON_MD_ERROR; const char* errIcon = ICON_MD_ERROR;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, errIcon); ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, errIcon);
fgDl->AddText(iconFont, iconFont->LegacySize, fgDl->AddText(iconFont, iconFont->LegacySize,
ImVec2(ix + schema::UI().drawElement("tabs.send", "error-icon-x-offset").size, iy + body2->LegacySize * 0.5f - iSz.y * 0.5f), ImVec2(ix + schema::UI().drawElement("tabs.send", "error-icon-x-offset").size * dp, iy + body2->LegacySize * 0.5f - iSz.y * 0.5f),
Error(), errIcon); Error(), errIcon);
} }
// Error text (wrapped) // Error text (wrapped)
fgDl->AddText(body2, body2->LegacySize, ImVec2(ix + schema::UI().drawElement("tabs.send", "error-text-x-offset").size, iy), Error(), fgDl->AddText(body2, body2->LegacySize, ImVec2(ix + schema::UI().drawElement("tabs.send", "error-text-x-offset").size * dp, iy), Error(),
s_tx_status.c_str(), s_tx_status.c_str() + s_tx_status.size(), textWrapW); s_tx_status.c_str(), s_tx_status.c_str() + s_tx_status.size(), textWrapW);
// Buttons row — use invisible window for interactive widgets on top of overlay // Buttons row — use invisible window for interactive widgets on top of overlay
float btnY = iy + textSz.y + Layout::spacingMd(); float btnY = iy + textSz.y + Layout::spacingMd();
ImGui::SetNextWindowPos(ImVec2(ix, btnY)); ImGui::SetNextWindowPos(ImVec2(ix, btnY));
ImGui::SetNextWindowSize(ImVec2(w - pad * 2, btnH + schema::UI().drawElement("tabs.send", "error-btn-area-padding").size)); ImGui::SetNextWindowSize(ImVec2(w - pad * 2, btnH + schema::UI().drawElement("tabs.send", "error-btn-area-padding").size * dp));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
@@ -606,7 +608,7 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
ImGuiWindowFlags_NoBringToFrontOnFocus); ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "error-btn-bg-alpha").size))); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "error-btn-bg-alpha").size)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "error-btn-hover-alpha").size))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "error-btn-hover-alpha").size)));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "error-btn-rounding").size); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "error-btn-rounding").size * dp);
if (TactileSmallButton(TR("send_copy_error"), schema::UI().resolveFont("button"))) { if (TactileSmallButton(TR("send_copy_error"), schema::UI().resolveFont("button"))) {
ImGui::SetClipboardText(s_tx_status.c_str()); ImGui::SetClipboardText(s_tx_status.c_str());
Notifications::instance().info(TR("send_error_copied")); Notifications::instance().info(TR("send_error_copied"));
@@ -629,8 +631,8 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
} }
// ---- SENDING / SUCCESS: inline progress card ---- // ---- SENDING / SUCCESS: inline progress card ----
float progCardH = schema::UI().drawElement("tabs.send", "progress-card-height").size; float progCardH = schema::UI().drawElement("tabs.send", "progress-card-height").size * dp;
float progCardHTxid = schema::UI().drawElement("tabs.send", "progress-card-height-txid").size; float progCardHTxid = schema::UI().drawElement("tabs.send", "progress-card-height-txid").size * dp;
float progH = s_result_txid.empty() ? progCardH : progCardHTxid; float progH = s_result_txid.empty() ? progCardH : progCardHTxid;
ImVec2 pMin(x, y); ImVec2 pMin(x, y);
ImVec2 pMax(x + w, y + progH); ImVec2 pMax(x + w, y + progH);
@@ -639,8 +641,8 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
progGlass.rounding = Layout::glassRounding() * schema::UI().drawElement("tabs.send", "progress-glass-rounding-ratio").size; progGlass.rounding = Layout::glassRounding() * schema::UI().drawElement("tabs.send", "progress-glass-rounding-ratio").size;
DrawGlassPanel(dl, pMin, pMax, progGlass); DrawGlassPanel(dl, pMin, pMax, progGlass);
float progPadX = schema::UI().drawElement("tabs.send", "progress-card-pad-x").size; float progPadX = schema::UI().drawElement("tabs.send", "progress-card-pad-x").size * dp;
float progPadY = schema::UI().drawElement("tabs.send", "progress-card-pad-y").size; float progPadY = schema::UI().drawElement("tabs.send", "progress-card-pad-y").size * dp;
float ix = pMin.x + progPadX; float ix = pMin.x + progPadX;
float iy = pMin.y + progPadY; float iy = pMin.y + progPadY;
char buf[128]; char buf[128];
@@ -654,7 +656,7 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
ImVec2(ix, iy), Primary(), spinIcon); ImVec2(ix, iy), Primary(), spinIcon);
double elapsed = ImGui::GetTime() - s_send_start_time; double elapsed = ImGui::GetTime() - s_send_start_time;
snprintf(buf, sizeof(buf), TR("send_submitting"), elapsed); snprintf(buf, sizeof(buf), TR("send_submitting"), elapsed);
dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size, iy), OnSurface(), buf); dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size * dp, iy), OnSurface(), buf);
} else { } else {
// Success checkmark // Success checkmark
ImFont* iconFont = material::Type().iconMed(); ImFont* iconFont = material::Type().iconMed();
@@ -662,19 +664,19 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, checkIcon); ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, checkIcon);
dl->AddText(iconFont, iconFont->LegacySize, dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(ix, iy), Success(), checkIcon); ImVec2(ix, iy), Success(), checkIcon);
dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size, iy), Success(), TR("send_tx_sent")); dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size * dp, iy), Success(), TR("send_tx_sent"));
if (!s_result_txid.empty()) { if (!s_result_txid.empty()) {
float txY = iy + body2->LegacySize + schema::UI().drawElement("tabs.send", "txid-y-offset").size; float txY = iy + body2->LegacySize + schema::UI().drawElement("tabs.send", "txid-y-offset").size * dp;
int txidThreshold = (int)schema::UI().drawElement("tabs.send", "txid-display-threshold").size; int txidThreshold = (int)schema::UI().drawElement("tabs.send", "txid-display-threshold").size;
int txidTruncLen = (int)schema::UI().drawElement("tabs.send", "txid-trunc-len").size; int txidTruncLen = (int)schema::UI().drawElement("tabs.send", "txid-trunc-len").size;
std::string dispTxid = (int)s_result_txid.length() > txidThreshold std::string dispTxid = (int)s_result_txid.length() > txidThreshold
? s_result_txid.substr(0, txidTruncLen) + "..." + s_result_txid.substr(s_result_txid.length() - txidTruncLen) ? s_result_txid.substr(0, txidTruncLen) + "..." + s_result_txid.substr(s_result_txid.length() - txidTruncLen)
: s_result_txid; : s_result_txid;
snprintf(buf, sizeof(buf), TR("send_txid_label"), dispTxid.c_str()); snprintf(buf, sizeof(buf), TR("send_txid_label"), dispTxid.c_str());
dl->AddText(capFont, capFont->LegacySize, ImVec2(ix + schema::UI().drawElement("tabs.send", "txid-label-x-offset").size, txY), dl->AddText(capFont, capFont->LegacySize, ImVec2(ix + schema::UI().drawElement("tabs.send", "txid-label-x-offset").size * dp, txY),
OnSurfaceDisabled(), buf); OnSurfaceDisabled(), buf);
ImGui::SetCursorScreenPos(ImVec2(pMax.x - schema::UI().drawElement("tabs.send", "txid-copy-btn-right-offset").size, txY - schema::UI().drawElement("tabs.send", "txid-copy-btn-y-offset").size)); ImGui::SetCursorScreenPos(ImVec2(pMax.x - schema::UI().drawElement("tabs.send", "txid-copy-btn-right-offset").size * dp, txY - schema::UI().drawElement("tabs.send", "txid-copy-btn-y-offset").size * dp));
if (TactileSmallButton(TR("copy"), schema::UI().resolveFont("button"))) { if (TactileSmallButton(TR("copy"), schema::UI().resolveFont("button"))) {
ImGui::SetClipboardText(s_result_txid.c_str()); ImGui::SetClipboardText(s_result_txid.c_str());
Notifications::instance().info(TR("send_txid_copied")); Notifications::instance().info(TR("send_txid_copied"));
@@ -726,6 +728,7 @@ void RenderSendConfirmPopup(App* app) {
float popupAvailW = ImGui::GetMainViewport()->Size.x * S.drawElement("tabs.send", "confirm-popup-width-ratio").size; float popupAvailW = ImGui::GetMainViewport()->Size.x * S.drawElement("tabs.send", "confirm-popup-width-ratio").size;
float popupW = std::min(schema::UI().drawElement("tabs.send", "confirm-popup-max-width").size, popupAvailW); float popupW = std::min(schema::UI().drawElement("tabs.send", "confirm-popup-max-width").size, popupAvailW);
float popVs = Layout::vScale(); float popVs = Layout::vScale();
const float dp = Layout::dpiScale();
material::OverlayDialogSpec ov; material::OverlayDialogSpec ov;
ov.title = TR("confirm_send"); ov.p_open = nullptr; ov.title = TR("confirm_send"); ov.p_open = nullptr;
ov.style = material::OverlayStyle::BlurFloat; ov.style = material::OverlayStyle::BlurFloat;
@@ -797,7 +800,12 @@ void RenderSendConfirmPopup(App* app) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_amount_details")); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_amount_details"));
ImVec2 cMin = ImGui::GetCursorScreenPos(); ImVec2 cMin = ImGui::GetCursorScreenPos();
// Floor the row step at the *actual* scaled caption-row height (+ a small dp-scaled
// gap) so the Fee/Total divider — drawn at rowStep*0.5 below the Fee baseline —
// clears the (already-DPI-scaled) fee text instead of striking through it at HiDPI.
// capFont->LegacySize is already DPI-scaled; do not scale it again.
float rowStep = std::max(schema::UI().drawElement("tabs.send", "confirm-row-step-min").size, schema::UI().drawElement("tabs.send", "confirm-row-step").size * popVs); float rowStep = std::max(schema::UI().drawElement("tabs.send", "confirm-row-step-min").size, schema::UI().drawElement("tabs.send", "confirm-row-step").size * popVs);
rowStep = std::max(rowStep, capFont->LegacySize + 6.0f * dp);
float configuredH = std::max(schema::UI().drawElement("tabs.send", "confirm-amount-card-min-height").size, schema::UI().drawElement("tabs.send", "confirm-amount-card-height").size * popVs); float configuredH = std::max(schema::UI().drawElement("tabs.send", "confirm-amount-card-min-height").size, schema::UI().drawElement("tabs.send", "confirm-amount-card-height").size * popVs);
float contentH = Layout::spacingMd() * 2.0f + capFont->LegacySize * 2.0f + sub1->LegacySize + rowStep * 2.0f; float contentH = Layout::spacingMd() * 2.0f + capFont->LegacySize * 2.0f + sub1->LegacySize + rowStep * 2.0f;
float cH = std::max(configuredH, contentH); float cH = std::max(configuredH, contentH);
@@ -936,9 +944,10 @@ static bool RenderZeroBalanceCTA(App* app, ImDrawList* dl, float width) {
ImFont* sub1 = Type().subtitle1(); ImFont* sub1 = Type().subtitle1();
ImFont* capFont = Type().caption(); ImFont* capFont = Type().caption();
const float dp = Layout::dpiScale();
ImVec2 ctaMin = ImGui::GetCursorScreenPos(); ImVec2 ctaMin = ImGui::GetCursorScreenPos();
float ctaH = schema::UI().drawElement("tabs.send", "cta-height").size; float ctaH = schema::UI().drawElement("tabs.send", "cta-height").size * dp;
ImVec2 ctaMax(ctaMin.x + width, ctaMin.y + ctaH); ImVec2 ctaMax(ctaMin.x + width, ctaMin.y + ctaH);
GlassPanelSpec ctaGlass; GlassPanelSpec ctaGlass;
ctaGlass.rounding = Layout::glassRounding(); ctaGlass.rounding = Layout::glassRounding();
@@ -952,7 +961,7 @@ static bool RenderZeroBalanceCTA(App* app, ImDrawList* dl, float width) {
TR("send_switch_to_receive")); TR("send_switch_to_receive"));
cy += capFont->LegacySize + Layout::spacingMd(); cy += capFont->LegacySize + Layout::spacingMd();
ImGui::SetCursorScreenPos(ImVec2(cx, cy)); ImGui::SetCursorScreenPos(ImVec2(cx, cy));
if (TactileButton(TR("send_go_to_receive"), ImVec2(schema::UI().drawElement("tabs.send", "cta-button-width").size, schema::UI().drawElement("tabs.send", "cta-button-height").size), schema::UI().resolveFont("button"))) { if (TactileButton(TR("send_go_to_receive"), ImVec2(schema::UI().drawElement("tabs.send", "cta-button-width").size * dp, schema::UI().drawElement("tabs.send", "cta-button-height").size * dp), schema::UI().resolveFont("button"))) {
app->setCurrentPage(NavPage::Receive); app->setCurrentPage(NavPage::Receive);
} }
ImGui::SetCursorScreenPos(ImVec2(ctaMin.x, ctaMax.y + Layout::spacingLg())); ImGui::SetCursorScreenPos(ImVec2(ctaMin.x, ctaMax.y + Layout::spacingLg()));

View File

@@ -148,7 +148,7 @@ void ShieldDialog::render(App* app)
// Fee // Fee
ImGui::Text("%s", TR("fee_label")); ImGui::Text("%s", TR("fee_label"));
ImGui::SetNextItemWidth(feeInput.width); 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");
if (s_fee < 0.0) s_fee = 0.0; // no negative fee if (s_fee < 0.0) s_fee = 0.0; // no negative fee
if (s_fee > 1.0) s_fee = 1.0; // guard a fat-fingered huge fee (mirrors utxo clamp) if (s_fee > 1.0) s_fee = 1.0; // guard a fat-fingered huge fee (mirrors utxo clamp)
@@ -159,7 +159,7 @@ void ShieldDialog::render(App* app)
// UTXO limit // UTXO limit
ImGui::Text("%s", TR("shield_utxo_limit")); ImGui::Text("%s", TR("shield_utxo_limit"));
ImGui::SetNextItemWidth(utxoInput.width); ImGui::SetNextItemWidth(utxoInput.width * Layout::dpiScale());
ImGui::InputInt("##Limit", &s_utxo_limit); ImGui::InputInt("##Limit", &s_utxo_limit);
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextDisabled("%s", TR("shield_max_utxos")); ImGui::TextDisabled("%s", TR("shield_max_utxos"));

View File

@@ -121,7 +121,7 @@ void TransactionDetailsDialog::render(App* app)
char txid_buf[128]; char txid_buf[128];
strncpy(txid_buf, tx.txid.c_str(), sizeof(txid_buf) - 1); strncpy(txid_buf, tx.txid.c_str(), sizeof(txid_buf) - 1);
txid_buf[sizeof(txid_buf) - 1] = '\0'; txid_buf[sizeof(txid_buf) - 1] = '\0';
ImGui::SetNextItemWidth(txidInput.width); ImGui::SetNextItemWidth(txidInput.width); // negative = fill, reserving |width| px for the Copy button (a content-region margin, not raw px — must NOT be dpi-scaled)
ImGui::InputText("##TxID", txid_buf, sizeof(txid_buf), ImGuiInputTextFlags_ReadOnly); ImGui::InputText("##TxID", txid_buf, sizeof(txid_buf), ImGuiInputTextFlags_ReadOnly);
ImGui::SameLine(); ImGui::SameLine();
if (material::StyledButton("Copy##TxID", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) { if (material::StyledButton("Copy##TxID", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
@@ -139,13 +139,14 @@ void TransactionDetailsDialog::render(App* app)
char addr_buf[512]; char addr_buf[512];
strncpy(addr_buf, tx.address.c_str(), sizeof(addr_buf) - 1); strncpy(addr_buf, tx.address.c_str(), sizeof(addr_buf) - 1);
addr_buf[sizeof(addr_buf) - 1] = '\0'; addr_buf[sizeof(addr_buf) - 1] = '\0';
// width is a negative fill-with-right-margin sentinel — keep unscaled; only height is raw px.
ImGui::InputTextMultiline("##Address", addr_buf, sizeof(addr_buf), ImGui::InputTextMultiline("##Address", addr_buf, sizeof(addr_buf),
ImVec2(addrInput.width, addrInput.height > 0 ? addrInput.height : 50), ImGuiInputTextFlags_ReadOnly); ImVec2(addrInput.width, (addrInput.height > 0 ? addrInput.height : 50) * Layout::dpiScale()), ImGuiInputTextFlags_ReadOnly);
} else { } else {
char addr_buf[128]; char addr_buf[128];
strncpy(addr_buf, tx.address.c_str(), sizeof(addr_buf) - 1); strncpy(addr_buf, tx.address.c_str(), sizeof(addr_buf) - 1);
addr_buf[sizeof(addr_buf) - 1] = '\0'; addr_buf[sizeof(addr_buf) - 1] = '\0';
ImGui::SetNextItemWidth(addrInput.width); ImGui::SetNextItemWidth(addrInput.width); // negative fill sentinel — must NOT be dpi-scaled
ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly); ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
} }
ImGui::SameLine(); ImGui::SameLine();