From ce8c7696d443ebbfefc4c4a3bb48ecc21edce5a0 Mon Sep 17 00:00:00 2001 From: DanS Date: Tue, 18 Aug 2026 21:29:10 -0500 Subject: [PATCH] =?UTF-8?q?fix(ui):=20finish=20HiDPI=20pass=20=E2=80=94=20?= =?UTF-8?q?cosmetic=20=C3=97dpiScale,=20narrow-width=20reflow,=20recent-li?= =?UTF-8?q?st=20reserves?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/app_security.cpp | 6 +++--- src/ui/pages/settings_page.cpp | 6 ++++-- src/ui/windows/balance_components.cpp | 12 ++++++++---- src/ui/windows/balance_tab.cpp | 2 +- src/ui/windows/chat_tab.cpp | 2 +- src/ui/windows/console_tab.cpp | 20 ++++++++++++++------ src/ui/windows/explorer_tab.cpp | 14 +++++++------- src/ui/windows/market_tab.cpp | 7 ++++--- src/ui/windows/mining_controls.cpp | 6 +++--- src/ui/windows/mining_earnings.cpp | 13 ++++++++++++- src/ui/windows/mining_mode_toggle.cpp | 4 ++-- src/ui/windows/receive_tab.cpp | 27 +++++++++++++++++++-------- src/ui/windows/send_tab.cpp | 16 ++++++++++------ src/ui/windows/transactions_tab.cpp | 14 ++++++++++++-- src/ui/windows/wallets_dialog.h | 23 ++++++++++++++++++----- 15 files changed, 118 insertions(+), 54 deletions(-) diff --git a/src/app_security.cpp b/src/app_security.cpp index 845894b..838bba2 100644 --- a/src/app_security.cpp +++ b/src/app_security.cpp @@ -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), diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 21b65c8..e7e1793 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -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); diff --git a/src/ui/windows/balance_components.cpp b/src/ui/windows/balance_components.cpp index f03949f..fb607ed 100644 --- a/src/ui/windows/balance_components.cpp +++ b/src/ui/windows/balance_components.cpp @@ -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; diff --git a/src/ui/windows/balance_tab.cpp b/src/ui/windows/balance_tab.cpp index a505bf2..c84be7e 100644 --- a/src/ui/windows/balance_tab.cpp +++ b/src/ui/windows/balance_tab.cpp @@ -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. diff --git a/src/ui/windows/chat_tab.cpp b/src/ui/windows/chat_tab.cpp index 8bea45b..e4e4142 100644 --- a/src/ui/windows/chat_tab.cpp +++ b/src/ui/windows/chat_tab.cpp @@ -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; diff --git a/src/ui/windows/console_tab.cpp b/src/ui/windows/console_tab.cpp index 45e8ff0..4648df9 100644 --- a/src/ui/windows/console_tab.cpp +++ b/src/ui/windows/console_tab.cpp @@ -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; diff --git a/src/ui/windows/explorer_tab.cpp b/src/ui/windows/explorer_tab.cpp index fd466aa..efe4c29 100644 --- a/src/ui/windows/explorer_tab.cpp +++ b/src/ui/windows/explorer_tab.cpp @@ -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++) { diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 5265d83..cdc2ffe 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -1347,9 +1347,10 @@ static void mktDrawPairSelector(App* app, const std::vector& 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(); diff --git a/src/ui/windows/mining_controls.cpp b/src/ui/windows/mining_controls.cpp index 37b16c0..7e7e2a7 100644 --- a/src/ui/windows/mining_controls.cpp +++ b/src/ui/windows/mining_controls.cpp @@ -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)); diff --git a/src/ui/windows/mining_earnings.cpp b/src/ui/windows/mining_earnings.cpp index 2bccb17..b7f3d31 100644 --- a/src/ui/windows/mining_earnings.cpp +++ b/src/ui/windows/mining_earnings.cpp @@ -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(); diff --git a/src/ui/windows/mining_mode_toggle.cpp b/src/ui/windows/mining_mode_toggle.cpp index 75029fb..2e70ba2 100644 --- a/src/ui/windows/mining_mode_toggle.cpp +++ b/src/ui/windows/mining_mode_toggle.cpp @@ -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(); diff --git a/src/ui/windows/receive_tab.cpp b/src/ui/windows/receive_tab.cpp index f4f0707..fff979e 100644 --- a/src/ui/windows/receive_tab.cpp +++ b/src/ui/windows/receive_tab.cpp @@ -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)); } diff --git a/src/ui/windows/send_tab.cpp b/src/ui/windows/send_tab.cpp index 343a632..6a92a54 100644 --- a/src/ui/windows/send_tab.cpp +++ b/src/ui/windows/send_tab.cpp @@ -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; diff --git a/src/ui/windows/transactions_tab.cpp b/src/ui/windows/transactions_tab.cpp index 6f57f83..728d2f9 100644 --- a/src/ui/windows/transactions_tab.cpp +++ b/src/ui/windows/transactions_tab.cpp @@ -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); diff --git a/src/ui/windows/wallets_dialog.h b/src/ui/windows/wallets_dialog.h index 66494b8..7d0ffd4 100644 --- a/src/ui/windows/wallets_dialog.h +++ b/src/ui/windows/wallets_dialog.h @@ -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();