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:
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user