fix(ui): finish HiDPI pass — cosmetic ×dpiScale, narrow-width reflow, recent-list reserves

The tail of the DPI/font-scale/responsiveness audit — ~26 remaining findings.

Container / recent-list (Theme-1 leftovers):
- Send: drop NoScrollbar|NoScrollWithMouse on ##SendFormScroll so Recent Sends is
  reachable at font_scale 1.5 (parity with receive).
- Receive: cap the QR/form card via std::min(mainCardTargetH, availH - recentReserve)
  so RECENT RECEIVED stays on-screen (identity at 1.0x).
- Wallets dialog: size the capped-mode list to whole rows so it no longer clips a
  partial row / crowds "Create a new wallet".

Narrow-width (1024px) reflow:
- Console toolbar reserves space for ALL trailing controls (both icon toggles + zoom
  buttons) so the +/- zoom no longer runs off-window.
- History sort combo sized to its measured widest localized label ("Newest first").
- Settings Theme/Layout/Language row: scale the wide→stacked breakpoint by dpiScale so
  it drops to full-width stacked combos at 1.5x (Consolidated Card no longer clips).
- Recent-tx type label: derive the address column X from the measured label width so it
  can't collide at narrow widths.
- Mining Recent Pool Payouts: floor the panel height to fit the empty-state caption.

Cosmetic ×dpiScale() on absolute geometry (no-ops at 1.0x): mining SOLO|POOL toggle &
idle combos, market pair-chips, password/PIN strength bars, receive/send currency
toggles, explorer search bar/rows/rounding, About-card logo, chat empty-state wrap,
recent-list address/time offsets, address-toolbar & two-row action buttons, console
line-gap/status-dot/pane rounding.

Verified at font_scale 1.5 and at 1024px across full-node + Lite + Windows (ctest
green) and an adversarial diff review (one over-reserve regression fixed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-18 21:29:10 -05:00
parent 99d1e73676
commit ce8c7696d4
15 changed files with 118 additions and 54 deletions

View File

@@ -1265,7 +1265,7 @@ void App::renderEncryptWalletDialog() {
else if (tier == 1) { strengthLabel = TR("wiz_strength_fair"); strengthCol = ImVec4(1,0.7f,0.3f,1); strengthPct = 0.5f; }
float barW = ImGui::GetContentRegionAvail().x;
float barH = 4.0f;
float barH = 4.0f * ui::Layout::dpiScale();
ImVec2 p = ImGui::GetCursorScreenPos();
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
@@ -1315,7 +1315,7 @@ void App::renderEncryptWalletDialog() {
// Indeterminate progress bar
{
float barW = ImGui::GetContentRegionAvail().x;
float barH = 6.0f;
float barH = 6.0f * ui::Layout::dpiScale();
ImVec2 p = ImGui::GetCursorScreenPos();
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
@@ -1815,7 +1815,7 @@ void App::renderDecryptWalletDialog() {
// Indeterminate progress bar
{
float barW = ImGui::GetContentRegionAvail().x;
float barH = 6.0f;
float barH = 6.0f * ui::Layout::dpiScale();
ImVec2 p = ImGui::GetCursorScreenPos();
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),

View File

@@ -664,7 +664,7 @@ void RenderSettingsPage(App* app) {
float contentW = availWidth - pad * 2;
float comboGap = S.drawElement("components.settings-page", "combo-row-gap").size;
float compactBP = S.drawElement("components.settings-page", "compact-breakpoint").size;
bool wideLayout = availWidth >= compactBP;
bool wideLayout = availWidth >= compactBP * dp; // scale the breakpoint so the 3-combo row drops to the stacked layout at high font scale (else the combos clip)
float refreshBtnW = S.drawElement("components.settings-page", "refresh-btn-width").size;
// --- Skin data ---
@@ -762,6 +762,8 @@ void RenderSettingsPage(App* app) {
float lblThemeW = ImGui::CalcTextSize(TR("theme")).x + lblGap;
float lblLayoutW = ImGui::CalcTextSize(TR("balance_layout")).x + lblGap;
float lblLangW = ImGui::CalcTextSize(TR("language")).x + lblGap;
// Budget matches the RAW draws below (SameLine(0, comboGap) and ImVec2(refreshBtnW, 0)) —
// don't dpi-scale these terms or the budget over-reserves and the combos shrink needlessly.
float totalFixed = lblThemeW + lblLayoutW + lblLangW
+ comboGap * 2 + Layout::spacingSm() + refreshBtnW;
float comboW = std::max(80.0f, (contentW - totalFixed) / 3.0f);
@@ -2533,7 +2535,7 @@ void RenderSettingsPage(App* app) {
ImVec2 logoPos = ImGui::GetCursorScreenPos();
float logoAspect = (app->getLogoHeight() > 0)
? (float)app->getLogoWidth() / (float)app->getLogoHeight() : 1.0f;
float logoReserveH = schema::UI().drawElement("components.settings-page", "about-logo-size").sizeOr(150.0f);
float logoReserveH = schema::UI().drawElement("components.settings-page", "about-logo-size").sizeOr(150.0f) * dp;
if (logoTex != 0) {
logoAreaW = logoReserveH * logoAspect + Layout::spacingLg();
ImGui::Indent(logoAreaW);

View File

@@ -188,8 +188,8 @@ void RenderSharedAddressList(App* app, float listH, float availW,
}
}
float buttonWidth = (addrBtn.width > 0) ? addrBtn.width : 140.0f;
float spacing = (addrBtn.gap > 0) ? addrBtn.gap : 8.0f;
float buttonWidth = ((addrBtn.width > 0) ? addrBtn.width : 140.0f) * dp;
float spacing = ((addrBtn.gap > 0) ? addrBtn.gap : 8.0f) * dp;
float totalButtonsWidth = buttonWidth * 2 + spacing;
float kMinButtonsPosition = std::max(S.drawElement("tabs.balance", "min-buttons-position").size,
S.drawElement("tabs.balance", "buttons-position").size * hs);
@@ -842,7 +842,11 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
dl->AddText(capFont, capFont->LegacySize,
ImVec2(tx_x, rowPos.y + 2 * dp), OnSurfaceMedium(), display.typeText.c_str());
float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f) * hs;
// Start the address column past the MEASURED type-label width (plus a fixed gap) so it can
// never overlap the label — a fixed schema offset shrinks below the label width at narrow
// widths (hs < 1) and collides. Mirrors how amtX/agoSz measure their own text below.
ImVec2 typeSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.typeText.c_str());
float addrX = tx_x + typeSz.x + Layout::spacingMd();
dl->AddText(capFont, capFont->LegacySize,
ImVec2(addrX, rowPos.y + 2 * dp), OnSurfaceDisabled(), display.addressText.c_str());
@@ -857,7 +861,7 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.timeText.c_str());
dl->AddText(capFont, capFont->LegacySize,
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f), rowPos.y + 2 * dp),
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f) * hs, rowPos.y + 2 * dp),
OnSurfaceDisabled(), display.timeText.c_str());
float rowW = ImGui::GetContentRegionAvail().x;

View File

@@ -1939,7 +1939,7 @@ static void RenderBalanceTwoRow(App* app) {
}
// Action buttons right-aligned
float btnW = S.drawElement("tabs.balance.two-row", "action-btn-width").sizeOr(80.0f);
float btnW = S.drawElement("tabs.balance.two-row", "action-btn-width").sizeOr(80.0f) * dp;
float rightEdge = ImGui::GetWindowWidth() - Layout::spacingLg();
ImGui::SameLine(rightEdge - btnW * 2 - Layout::spacingSm());
// Stable ## ids keep the button identity fixed across translations.

View File

@@ -547,7 +547,7 @@ void centeredEmptyState(const char* icon, const char* title, const char* hint) {
ImFont* titleF = material::Type().subtitle1();
ImFont* hintF = material::Type().body2();
const float gap = 8.0f * Layout::dpiScale();
const float wrap = std::min(avail.x - 40.0f, 360.0f);
const float wrap = std::min(avail.x - 40.0f * Layout::dpiScale(), 360.0f * Layout::dpiScale());
const float iconSz = iconF ? scaledSize(iconF) : 40.0f;
const float iconH = iconF ? iconF->CalcTextSizeA(iconSz, FLT_MAX, 0.0f, icon).y : 0.0f;
const float titleH = titleF->CalcTextSizeA(scaledSize(titleF), FLT_MAX, 0.0f, title).y;

View File

@@ -617,7 +617,7 @@ void ConsoleTab::drawToolbarStatus(ConsoleCommandExecutor& exec)
ConsoleStatusLine st = exec.toolbarStatus();
if (!st.text.empty()) {
ImVec2 cp = ImGui::GetCursorScreenPos();
float dotR = schema::UI().drawElement("tabs.console", "status-dot-radius-base").size + schema::UI().drawElement("tabs.console", "status-dot-radius-scale").size * Layout::hScale();
float dotR = (schema::UI().drawElement("tabs.console", "status-dot-radius-base").size + schema::UI().drawElement("tabs.console", "status-dot-radius-scale").size) * Layout::hScale();
float dotY = cp.y + ImGui::GetTextLineHeight() * 0.5f;
float dotX = cp.x + dotR + 2.0f * Layout::dpiScale();
@@ -687,9 +687,14 @@ void ConsoleTab::drawLogFilterToggles(const ConsoleLogFilterCaps& caps)
void ConsoleTab::drawFilterInput()
{
using namespace material;
float zoomBtnSpace = ImGui::GetFrameHeight() * 2.0f + Layout::spacingSm() * 3.0f;
float filterAvail = ImGui::GetContentRegionAvail().x - zoomBtnSpace;
float filterW = std::min(schema::UI().drawElement("tabs.console", "filter-max-width").size, filterAvail * schema::UI().drawElement("tabs.console", "filter-width-ratio").size);
// Reserve room for EVERY trailing same-line control drawn AFTER this filter on the toolbar
// row: the two icon toggles (accent-fill + text-color) and the two zoom buttons, plus the
// group spacers between them. Otherwise the filter eats the row and the trailing controls
// run off-window (worst at 1024px / font_scale 1.5). All four are GetFrameHeight() wide.
float trailingBtnSpace = ImGui::GetFrameHeight() * 4.0f + Layout::spacingSm() * 7.0f;
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);
ImGui::SetNextItemWidth(filterW);
ImGui::InputTextWithHint("##ConsoleFilter", TR("console_filter_hint"), filter_text_, sizeof(filter_text_));
if (filter_text_[0] != '\0') {
@@ -768,7 +773,10 @@ void ConsoleTab::renderOutput()
// height. The inter-line gap is added explicitly to layout_.heights
// so that layout_.cumulativeY stays perfectly in sync with actual
// cursor positions (avoiding selection-offset drift).
float interLineGap = S.drawElement("tabs.console", "output").getFloat("line-spacing", 0.0f);
// Raw logical px from the schema; scale it so the inter-line gap grows at font_scale 1.5
// (it is added to the already-DPI-scaled GetTextLineHeight in BuildConsoleLayout — scale the
// gap only, never the line height).
float interLineGap = S.drawElement("tabs.console", "output").getFloat("line-spacing", 0.0f) * Layout::dpiScale();
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
// Inner padding for glass panel
@@ -1894,7 +1902,7 @@ void ConsoleTab::renderCommandsPopup(ConsoleCommandExecutor& exec)
// Both panes sit on soft Material glass surfaces (no hard 1px child border) with inner padding.
GlassPanelSpec paneGlass;
paneGlass.rounding = 14.0f;
paneGlass.rounding = 14.0f * dp;
paneGlass.fillAlpha = 30;
paneGlass.borderAlpha = 30;

View File

@@ -446,10 +446,10 @@ static void renderSearchBar(App* app, float availWidth) {
float navW = navBtnSz * 2.0f + pageW + navGap * 2.0f;
float inputW = std::min(
S.drawElement("tabs.explorer", "search-input-width").size,
S.drawElement("tabs.explorer", "search-input-width").size * Layout::dpiScale(),
availWidth * 0.65f);
float btnW = S.drawElement("tabs.explorer", "search-button-width").size;
float barH = S.drawElement("tabs.explorer", "search-bar-height").size;
float btnW = S.drawElement("tabs.explorer", "search-button-width").size * Layout::dpiScale();
float barH = S.drawElement("tabs.explorer", "search-bar-height").size * Layout::dpiScale();
// Clamp so search bar never overflows
float maxInputW = availWidth - btnW - navW - pad * 4 - Type().iconMed()->LegacySize;
@@ -755,8 +755,8 @@ static void renderRecentBlocks(App* app, float availWidth) {
ImFont* body2 = Type().body2();
ImFont* sub1 = Type().subtitle1();
float baseRowH = S.drawElement("tabs.explorer", "row-height").size;
float rowRound = S.drawElement("tabs.explorer", "row-rounding").size;
float baseRowH = S.drawElement("tabs.explorer", "row-height").size * dp;
float rowRound = S.drawElement("tabs.explorer", "row-rounding").size * dp;
float headerH = ovFont->LegacySize + Layout::spacingSm() + pad * 0.5f;
// Stretch card to fill the remaining tab height; rows scroll inside.
@@ -989,7 +989,7 @@ static void renderRecentBlocks(App* app, float availWidth) {
ImGui::EndChild();
float fadeZone = S.drawElement("tabs.explorer", "scroll-fade-zone").size;
float fadeZone = S.drawElement("tabs.explorer", "scroll-fade-zone").size * dp;
ApplyScrollEdgeMask(dl, parentVtx, childDL, childVtx,
rowAreaTop, rowAreaTop + rowAreaH, fadeZone, scrollY, scrollMaxY);
@@ -1179,7 +1179,7 @@ static void renderBlockDetailModal(App* app) {
ImGui::Spacing();
float txRowH = S.drawElement("tabs.explorer", "tx-row-height").size;
float txRowH = S.drawElement("tabs.explorer", "tx-row-height").size * dp;
ImU32 linkCol = schema::UI().resolveColor("var(--secondary-light)");
for (int i = 0; i < (int)s_detail_txids.size(); i++) {

View File

@@ -1347,9 +1347,10 @@ static void mktDrawPairSelector(App* app, const std::vector<data::ExchangeInfo>&
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.market", "exchange-top-gap").size));
{
float chipH = S.drawElement("tabs.market", "pair-chip-height").height;
float chipR = S.drawElement("tabs.market", "pair-chip-radius").radius;
float chipSpacing = S.drawElement("tabs.market", "pair-chip-spacing").size;
float dp = Layout::dpiScale();
float chipH = S.drawElement("tabs.market", "pair-chip-height").height * dp;
float chipR = S.drawElement("tabs.market", "pair-chip-radius").radius * dp;
float chipSpacing = S.drawElement("tabs.market", "pair-chip-spacing").size * dp;
float innerGap = Layout::spacingSm();
float sidePad = Layout::spacingMd();

View File

@@ -293,7 +293,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
for (const auto& d : delays) {
if (d.seconds == curDelay) { previewLabel = d.label; break; }
}
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f) * dp;
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
@@ -332,7 +332,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
if (curVal <= 0) curVal = hwThreads;
char previewBuf[16];
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f) * dp;
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
@@ -371,7 +371,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
if (curVal <= 0) curVal = std::max(1, hwThreads / 2);
char previewBuf[16];
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f) * dp;
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));

View File

@@ -533,7 +533,18 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
float recentAvailH = ImGui::GetContentRegionAvail().y - sHdr - gapOver;
float minRows = recentMined.empty() ? 2.0f : (float)recentMined.size();
float contentH_blocks = rowH_blocks * minRows + pad * 2.5f;
float recentH = std::clamp(contentH_blocks, 30.0f * dp, std::max(30.0f * dp, recentAvailH));
// Lower bound for the panel height. When the list is empty the centred empty-state
// (top pad + icon + gap + caption + bottom pad) needs more vertical room than the bare
// 30*dp floor, otherwise the caption is clipped once the thread-tile grid wraps to two
// rows and recentAvailH shrinks (font metrics/Layout helpers are already DPI-scaled — do
// not multiply them by dp; pad is a scaled param).
float recentMinH = 30.0f * dp;
if (recentMined.empty()) {
recentMinH = std::max(recentMinH,
pad * 0.5f + Type().iconMed()->LegacySize + Layout::spacingXs()
+ capFont->LegacySize + pad * 0.5f);
}
float recentH = std::clamp(contentH_blocks, recentMinH, std::max(recentMinH, recentAvailH));
// Glass panel wrapping the list + scroll-edge mask state
ImVec2 recentPanelMin = ImGui::GetCursorScreenPos();

View File

@@ -58,8 +58,8 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
const bool soloMiningAvailable = app->supportsSoloMining();
float toggleW = schema::UI().drawElement("tabs.mining", "mode-toggle-width").size * hs;
float toggleH = schema::UI().drawElement("tabs.mining", "mode-toggle-height").size;
float toggleRnd = schema::UI().drawElement("tabs.mining", "mode-toggle-rounding").size;
float toggleH = schema::UI().drawElement("tabs.mining", "mode-toggle-height").size * hs;
float toggleRnd = schema::UI().drawElement("tabs.mining", "mode-toggle-rounding").size * hs;
float totalW = soloMiningAvailable ? (toggleW * 2) : toggleW;
ImVec2 tMin = ImGui::GetCursorScreenPos();

View File

@@ -405,7 +405,7 @@ static void RenderRecentReceived(const AddressInfo& /* addr */,
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);
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f) * hs;
std::string addrDisplay = util::truncateMiddle(tx.address,
(int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
rowDL->AddText(capFont, capFont->LegacySize,
@@ -424,7 +424,7 @@ static void RenderRecentReceived(const AddressInfo& /* addr */,
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),
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());
@@ -488,6 +488,16 @@ void RenderReceiveTab(App* app)
float groupStartY = ImGui::GetCursorPosY();
float contentStartY = ImGui::GetCursorPosY();
// Reserve a slice of the available height for RECENT RECEIVED (ratio — mirrors
// balance_tab's recent-tx-reserve). The main card's target height is capped so it can
// never grow past (available - reserve): a no-op at 1.0x (mainCardTargetH already fits
// well within scrollAvailH there), but at HiDPI it prevents the card — whose QR (280*dp)
// and pads scale with dp while scrollAvailH is physical px — from eating the whole child
// and evicting the list.
float recvReserveRatio = S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
float recvRecentReserve = std::max(0.0f, scrollAvailH) * recvReserveRatio;
float recvCardCapH = std::max(0.0f, scrollAvailH - recvRecentReserve);
float formAvailW = ImGui::GetContentRegionAvail().x;
float formW = formAvailW;
ImGui::BeginGroup();
@@ -603,7 +613,7 @@ void RenderReceiveTab(App* app)
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Amount input with currency toggle
float toggleW = S.drawElement("tabs.receive", "currency-toggle-width").size;
float toggleW = S.drawElement("tabs.receive", "currency-toggle-width").size * Layout::dpiScale();
float amtInputW = addrColW - toggleW - Layout::spacingMd();
if (amtInputW < S.drawElement("tabs.receive", "amount-input-min-width").size) amtInputW = S.drawElement("tabs.receive", "amount-input-min-width").size;
double usd_price = state.market.price_usd;
@@ -667,8 +677,8 @@ void RenderReceiveTab(App* app)
float bH = bMax.y - bMin.y;
ImFont* font = ImGui::GetFont();
ImVec2 textSz = font->CalcTextSizeA(font->LegacySize, 10000, 0, currLabel);
float iconW = schema::UI().drawElement("tabs.receive", "currency-icon-width").size;
float iconGap2 = schema::UI().drawElement("tabs.receive", "currency-icon-gap").size;
float iconW = schema::UI().drawElement("tabs.receive", "currency-icon-width").size * Layout::dpiScale();
float iconGap2 = schema::UI().drawElement("tabs.receive", "currency-icon-gap").size * Layout::dpiScale();
float totalW2 = iconW + iconGap2 + textSz.x;
float startX = bMin.x + ((bMax.x - bMin.x) - totalW2) * 0.5f;
float cy = bMin.y + bH * 0.5f;
@@ -866,7 +876,7 @@ void RenderReceiveTab(App* app)
S.drawElement("tabs.receive", "action-btn-height").size * vScale);
float footerH = innerGap + actionBtnH + pad;
float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
float targetCardH = Layout::mainCardTargetH(formW, vScale);
float targetCardH = std::min(Layout::mainCardTargetH(formW, vScale), recvCardCapH);
float footerTopH = targetCardH - footerH;
if (currentCardH < footerTopH) {
ImGui::Dummy(ImVec2(0, footerTopH - currentCardH));
@@ -959,10 +969,11 @@ void RenderReceiveTab(App* app)
ImGui::Dummy(ImVec2(0, pad));
ImGui::Unindent(pad);
// Enforce shared card height (matches QR-driven target)
// Enforce shared card height (matches QR-driven target), capped so the reserved
// RECENT RECEIVED slice below stays on-screen at HiDPI.
{
float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
float targetCardH = Layout::mainCardTargetH(formW, vScale);
float targetCardH = std::min(Layout::mainCardTargetH(formW, vScale), recvCardCapH);
if (currentCardH < targetCardH)
ImGui::Dummy(ImVec2(0, targetCardH - currentCardH));
}

View File

@@ -1154,7 +1154,7 @@ static void RenderRecentSends(const WalletState& state, float width, ImFont* cap
ImVec2(txX, rowPos.y + 2.0f * dp), OnSurfaceMedium(), TR("sent_type"));
// Address (second line)
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f) * hs;
std::string addrDisplay = util::truncateMiddle(tx.address,
(int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
rowDL->AddText(capFont, capFont->LegacySize,
@@ -1173,7 +1173,7 @@ static void RenderRecentSends(const WalletState& state, float width, ImFont* cap
std::string ago = timeAgo(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),
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());
@@ -1251,8 +1251,12 @@ void RenderSendTab(App* app)
// SCROLLABLE CONTENT
// ================================================================
ImVec2 formAvail = ImGui::GetContentRegionAvail();
// NOTE: no NoScrollbar/NoScrollWithMouse here (mirrors receive_tab's ##ReceiveScroll).
// At font_scale 1.5 the form card grows to mainCardTargetH and pushes the appended
// "Recent Sends" list below the fold; letting this child scroll keeps it reachable.
// No-op at 1.0x where the content already fits (no scrollbar appears).
ImGui::BeginChild("##SendFormScroll", formAvail, false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGuiWindowFlags_NoBackground);
dl = ImGui::GetWindowDrawList();
// Top-aligned content — consistent vertical position across all tabs
@@ -1428,7 +1432,7 @@ void RenderSendTab(App* app)
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Toggle between DRGX and USD input
float toggleW = schema::UI().drawElement("tabs.send", "toggle-currency-width").size;
float toggleW = schema::UI().drawElement("tabs.send", "toggle-currency-width").size * Layout::dpiScale();
float amtInputW = colW - toggleW - Layout::spacingMd();
if (amtInputW < schema::UI().drawElement("tabs.send", "amount-input-min-width").size) amtInputW = schema::UI().drawElement("tabs.send", "amount-input-min-width").size;
@@ -1501,8 +1505,8 @@ void RenderSendTab(App* app)
float bH = bMax.y - bMin.y;
ImFont* font = ImGui::GetFont();
ImVec2 textSz = font->CalcTextSizeA(font->LegacySize, 10000, 0, currLabel);
float iconW = schema::UI().drawElement("tabs.send", "swap-icon-width").size;
float iconGap = schema::UI().drawElement("tabs.send", "swap-icon-gap").size;
float iconW = schema::UI().drawElement("tabs.send", "swap-icon-width").size * Layout::dpiScale();
float iconGap = schema::UI().drawElement("tabs.send", "swap-icon-gap").size * Layout::dpiScale();
float totalW = iconW + iconGap + textSz.x;
float startX = bMin.x + ((bMax.x - bMin.x) - totalW) * 0.5f;
float cy = bMin.y + bH * 0.5f;

View File

@@ -293,11 +293,21 @@ void RenderTransactionsTab(App* app)
TR("mined_filter"), TR("chat_filter") };
ImGui::Combo("##TxType", &type_filter, types, IM_ARRAYSIZE(types));
// Sort selector
// Sort selector — the sort labels ("Newest first"/localized) run longer than the type
// labels, so this combo needs its own width. Size it to the widest localized sort option
// measured with the active (default) font — the same font Combo renders with — plus ImGui's
// combo chrome (2x horizontal frame padding + the dropdown arrow button, GetFrameHeight()).
// CalcTextSize and the style paddings are already DPI-scaled, so this grows at font_scale 1.5
// without any hs multiply; clamp to at least the shared type-filter comboW.
ImGui::SameLine(0, filterGap);
ImGui::SetNextItemWidth(comboW);
const char* sorts[] = { TR("sort_date_newest"), TR("sort_date_oldest"),
TR("sort_amount_high"), TR("sort_amount_low") };
float sortTextW = 0.0f;
for (const char* s : sorts) sortTextW = std::max(sortTextW, ImGui::CalcTextSize(s).x);
float sortComboW = std::max(comboW,
sortTextW + ImGui::GetStyle().FramePadding.x * 2.0f
+ ImGui::GetFrameHeight());
ImGui::SetNextItemWidth(sortComboW);
ImGui::Combo("##TxSort", &s_sort_mode, sorts, IM_ARRAYSIZE(sorts));
ImGui::SameLine(0, filterGap);

View File

@@ -200,7 +200,20 @@ public:
// create/scan/footer controls so it scrolls internally and those stay visible. Uncapped,
// it equals the content height exactly (no spurious scrollbar — the common case).
float listHFit = listH;
if (capped) listHFit = std::max(walRowH, ImGui::GetContentRegionAvail().y - belowH);
if (capped) {
// Fill the height left above the pinned controls, but clip on a CLEAN row boundary:
// floor the available space to a whole number of card strides (walRowH + cardGap, the
// per-card advance from the loop) so the list never ends mid-row. Keep the same bottom
// pad as the uncapped listH so the last visible card stays off the clip edge.
const float stride = walRowH + cardGap;
const float avail = ImGui::GetContentRegionAvail().y - belowH;
// N rows occupy N*walRowH + (N-1)*cardGap = N*stride - cardGap; solve for the most whole
// rows that fit (a non-negative cast truncates toward zero = floor here), clamped to at
// least one so a tiny viewport still shows a row.
int nRows = stride > 0.0f ? (int)((avail + cardGap) / stride) : 1;
if (nRows < 1) nRows = 1;
listHFit = (float)nRows * stride - cardGap + Layout::spacingSm();
}
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
// NoScrollWithMouse + ApplySmoothScroll gives the wheel the same eased scrolling as the
// Settings page (ApplySmoothScroll handles the wheel itself, so let it own that input).
@@ -253,13 +266,13 @@ public:
bool hov = ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(rMin, rMax);
// Card background + state
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = hov ? 40 : 26; g.borderAlpha = 45;
GlassPanelSpec g; g.rounding = 10.0f * dp; g.fillAlpha = hov ? 40 : 26; g.borderAlpha = 45;
DrawGlassPanel(dl, rMin, rMax, g);
if (isCurrent) {
dl->AddRectFilled(rMin, rMax, WithAlpha(Success(), 16), 10.0f);
dl->AddRect(rMin, rMax, WithAlpha(Success(), 130), 10.0f, 0, 1.6f * dp);
dl->AddRectFilled(rMin, rMax, WithAlpha(Success(), 16), 10.0f * dp);
dl->AddRect(rMin, rMax, WithAlpha(Success(), 130), 10.0f * dp, 0, 1.6f * dp);
} else if (hov) {
dl->AddRect(rMin, rMax, WithAlpha(OnSurface(), 70), 10.0f, 0, 1.0f);
dl->AddRect(rMin, rMax, WithAlpha(OnSurface(), 70), 10.0f * dp, 0, 1.0f);
}
const float padX = Layout::spacingMd();