From b37d3d97b63b42df8b90a0070ad7daf8af15bf79 Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 19 Aug 2026 16:10:02 -0500 Subject: [PATCH] fix(send/receive): unify card width, justify receive footer, fix recipient-row button height/clip/glyph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Send and Receive are now consistent in layout, and the Send recipient row's buttons render correctly. Card envelope (Send ⇄ Receive consistency): - Add Layout::mainComposeCardBox(availW) — a single shared source for the compose card's width + centering (fill the available column up to content-max-width, then center). Both tabs derive their card from it, so they can't drift again. Previously Send capped at 760dp and Receive at 860dp, so the Send card rendered ~150px narrower on any window wider than ~860dp; now they fill available width identically. Receive: - Justify the footer buttons edge-to-edge (equal shares over the live count) instead of left-clustering with dead space, matching Send's full-width footer rhythm. - Build the address-dropdown preview to the combo's real pixel width so the trailing balance ("— 12.00000000 DRGX") no longer hard-clips at 150% (was char-count truncation). Send recipient row (input | Paste | contacts-icon): - Pin the contacts icon button to the frame height so the larger iconMed font doesn't auto-size it taller than Paste/the input. - Reserve the real ItemSpacing.x gaps (not the smaller spacingSm token) so the row no longer overshoots the card and clips the icon's right border. draw_helpers (root cause, app-wide): - TactileButton's icon path measured/drew the label INCLUDING the "##id" suffix (which CalcTextSizeA/AddText don't strip the way ImGui's text render does), shoving the glyph off-center-left. Strip at "##" before measuring/drawing. Corrects any icon button that passes an explicit size and a "##id" label; no-op for labels without "##". Verified via headless sweeps at 1.0x and 1.5x, plus a real 3800px-wide render (both cards byte-identical at L=1174/R=2773). ctest 1/1. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/layout.h | 11 +++++++++ src/ui/material/draw_helpers.h | 11 ++++++--- src/ui/windows/receive_tab.cpp | 44 +++++++++++++++++++++++++--------- src/ui/windows/send_tab.cpp | 25 ++++++++++++------- 4 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/ui/layout.h b/src/ui/layout.h index 83bbf52..397e65e 100644 --- a/src/ui/layout.h +++ b/src/ui/layout.h @@ -179,6 +179,17 @@ inline float kSidePanelWidthRatio() { return schema::UI().drawElement("panels", // [layout] content-max-width; default is generous so data-dense screens stay comfortable. inline float kContentMaxWidth() { return schema::UI().drawElement("layout", "content-max-width").sizeOr(1600.0f) * dpiScale(); } +// Shared compose-card envelope for the Send + Receive tabs (and any tab wanting the same box): fill the +// available column up to the content-max-width cap, then center the leftover as margin. Both tabs MUST +// derive their card width/offset from this so the two envelopes stay byte-for-byte identical — they +// previously drifted (Send capped at 760dp, Receive at 860dp), so the Send card rendered narrower than +// Receive on any window wider than ~860dp. Returns {width, offsetX} in the same units as availW. +struct CardBox { float width; float offsetX; }; +inline CardBox mainComposeCardBox(float availW) { + float w = std::min(availW, kContentMaxWidth()); + return CardBox{ w, std::max(0.0f, (availW - w) * 0.5f) }; +} + inline float kTableMinHeight() { return schema::UI().drawElement("panels", "table").getFloat("min-height", 150.0f) * dpiScale(); } inline float kTableHeightRatio() { return schema::UI().drawElement("panels", "table").getFloat("height-ratio", 0.45f); } diff --git a/src/ui/material/draw_helpers.h b/src/ui/material/draw_helpers.h index 3d8dbf1..688fcfd 100644 --- a/src/ui/material/draw_helpers.h +++ b/src/ui/material/draw_helpers.h @@ -544,11 +544,16 @@ inline bool TactileButton(const char* label, const ImVec2& size = ImVec2(0, 0), ImVec2 bMin = ImGui::GetItemRectMin(); ImVec2 bMax = ImGui::GetItemRectMax(); - // For icon fonts, manually draw centered icon after getting button rect + // For icon fonts, manually draw centered icon after getting button rect. Measure/draw only the + // VISIBLE label (up to the "##id" separator): CalcTextSizeA/AddText don't strip "##" the way + // ImGui's own text render does, so an id suffix like "##pickContact" would inflate textSz and + // shove the glyph left off-center (and try to draw the notdef id chars). if (isIconFont && size.x > 0 && size.y > 0) { - ImVec2 textSz = useFont->CalcTextSizeA(useFont->LegacySize, FLT_MAX, 0, label); + const char* labelEnd = label; + while (*labelEnd && !(labelEnd[0] == '#' && labelEnd[1] == '#')) ++labelEnd; + ImVec2 textSz = useFont->CalcTextSizeA(useFont->LegacySize, FLT_MAX, 0, label, labelEnd); ImVec2 textPos(bMin.x + (size.x - textSz.x) * 0.5f, bMin.y + (size.y - textSz.y) * 0.5f); - dl->AddText(useFont, useFont->LegacySize, textPos, ImGui::GetColorU32(ImGuiCol_Text), label); + dl->AddText(useFont, useFont->LegacySize, textPos, ImGui::GetColorU32(ImGuiCol_Text), label, labelEnd); } float rounding = ImGui::GetStyle().FrameRounding; diff --git a/src/ui/windows/receive_tab.cpp b/src/ui/windows/receive_tab.cpp index 3f209a1..6464539 100644 --- a/src/ui/windows/receive_tab.cpp +++ b/src/ui/windows/receive_tab.cpp @@ -175,6 +175,11 @@ static void RenderAddressDropdown(App* app, float width) { } } + // Combo/button widths first — the preview truncates to the combo's real pixel width below. + float copyBtnW = std::max(schema::UI().drawElement("tabs.receive", "copy-btn-min-width").size, schema::UI().drawElement("tabs.receive", "copy-btn-width").size * Layout::hScale(width)); + float newBtnW = std::max(schema::UI().drawElement("tabs.receive", "new-btn-min-width").size, schema::UI().drawElement("tabs.receive", "new-btn-width").size * Layout::hScale(width)); + float dropdownW = width - copyBtnW - newBtnW - Layout::spacingSm() * 2; + // Build preview string if (!app->isConnected()) { s_source_preview = TR(app->isLiteBuild() ? "lite_no_wallet_short" : "not_connected"); @@ -183,18 +188,27 @@ static void RenderAddressDropdown(App* app, float width) { const auto& addr = state.addresses[s_selected_address_idx]; bool isZ = addr.type == "shielded"; const char* tag = isZ ? "[Z]" : "[T]"; - std::string trunc = util::truncateMiddle(addr.address, - static_cast(std::max(schema::UI().drawElement("tabs.receive", "addr-preview-trunc-min").size, width / schema::UI().drawElement("tabs.receive", "addr-preview-trunc-divisor").size))); - snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s", - tag, trunc.c_str(), addr.balance, DRAGONX_TICKER); + // Reserve pixel room for the tag prefix and the trailing balance, then middle-truncate the + // address to whatever remains — measured with the combo's own Body2 font. Char-count + // truncation kept MORE chars as the column widened, so at 150% the scaled font overflowed + // and the combo hard-clipped "— 12.00000000 DRGX" to "— 1"; measuring in pixels keeps the + // balance visible at any scale. + ImFont* comboFont = Type().getFont(TypeStyle::Body2); + float comboFontSz = comboFont->LegacySize; + char prefix[16]; snprintf(prefix, sizeof(prefix), "%s ", tag); + char suffix[64]; snprintf(suffix, sizeof(suffix), " \xe2\x80\x94 %.8f %s", addr.balance, DRAGONX_TICKER); + float fixedW = comboFont->CalcTextSizeA(comboFontSz, FLT_MAX, 0.0f, prefix).x + + comboFont->CalcTextSizeA(comboFontSz, FLT_MAX, 0.0f, suffix).x; + // Combo interior = dropdownW minus its dropdown-arrow button (~frame height) and both frame paddings. + float addrBudget = dropdownW - ImGui::GetFrameHeight() - ImGui::GetStyle().FramePadding.x * 2.0f - fixedW; + if (addrBudget < 24.0f) addrBudget = 24.0f; // floor: truncate to a stub rather than overflow + std::string trunc = material::TruncateToWidth(addr.address, comboFont, comboFontSz, addrBudget); + snprintf(buf, sizeof(buf), "%s%s%s", prefix, trunc.c_str(), suffix); s_source_preview = buf; } else { s_source_preview = TR("select_receiving_address"); } - float copyBtnW = std::max(schema::UI().drawElement("tabs.receive", "copy-btn-min-width").size, schema::UI().drawElement("tabs.receive", "copy-btn-width").size * Layout::hScale(width)); - float newBtnW = std::max(schema::UI().drawElement("tabs.receive", "new-btn-min-width").size, schema::UI().drawElement("tabs.receive", "new-btn-width").size * Layout::hScale(width)); - float dropdownW = width - copyBtnW - newBtnW - Layout::spacingSm() * 2; ImGui::SetNextItemWidth(dropdownW); ImGui::PushFont(Type().getFont(TypeStyle::Body2)); if (ImGui::BeginCombo("##RecvAddr", s_source_preview.c_str())) { @@ -584,12 +598,14 @@ void RenderReceiveTab(App* app) // ================================================================ { // Cap + center the card so the address/amount column stops stretching while the - // QR plateaus. cardW never exceeds formW (only shrinks); leftover becomes margin. + // QR plateaus. Shared with the Send tab via mainComposeCardBox() so the two card + // envelopes are identical (they must never drift in width/position again). // The RECENT RECEIVED list below stays on the uncapped formW (handled separately). float cardDp = Layout::dpiScale(); - float cardW = std::min(formW, 860.0f * cardDp); + Layout::CardBox cardBox = Layout::mainComposeCardBox(formW); + float cardW = cardBox.width; float cardLeftX = ImGui::GetCursorScreenPos().x; - float cardOffsetX = std::max(0.0f, (formW - cardW) * 0.5f); + float cardOffsetX = cardBox.offsetX; if (cardOffsetX > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + cardOffsetX); @@ -911,7 +927,13 @@ void RenderReceiveTab(App* app) { float btnGap = Layout::spacingMd(); float btnH = std::max(schema::UI().drawElement("tabs.receive", "action-btn-min-height").size, schema::UI().drawElement("tabs.receive", "action-btn-height").size * vScale); - float otherBtnW = std::max(S.drawElement("tabs.receive", "action-btn-min-width").size, innerW * S.drawElement("tabs.receive", "action-btn-width-ratio").size); + // Justify the footer edge-to-edge like Send's [Review Send][Cancel] row instead of packing + // fixed-width buttons from the left (which left a large dead gap on the right). Split innerW + // into equal shares over the live button count: 2 by default (Clear Request + Explorer), + // 4 when an amount is requested (+ Copy URI + Share). + int nBtns = (s_request_amount > 0 ? 2 : 0) + 2; + float otherBtnW = std::max(S.drawElement("tabs.receive", "action-btn-min-width").size, + (innerW - (nBtns - 1) * btnGap) / (float)nBtns); bool firstBtn = true; diff --git a/src/ui/windows/send_tab.cpp b/src/ui/windows/send_tab.cpp index 3d30188..b9609ec 100644 --- a/src/ui/windows/send_tab.cpp +++ b/src/ui/windows/send_tab.cpp @@ -1272,12 +1272,13 @@ void RenderSendTab(App* app) float contentStartY = ImGui::GetCursorPosY(); float formAvailW = ImGui::GetContentRegionAvail().x; - // The compose form reads best as a centered fixed-width column, not edge-to-edge. - // Cap the card to a readable form width and center it; the recent-sends list below - // deliberately keeps the full column width (formAvailW). - const float sendDp = Layout::dpiScale(); - float formCardW = std::min(formAvailW, 760.0f * sendDp); - float formOffsetX = std::max(0.0f, (formAvailW - formCardW) * 0.5f); + // Fill the available column up to the content-max-width cap, then center. Shared with the + // Receive tab via mainComposeCardBox() so the two card envelopes are identical in width and + // position (Send previously capped at 760dp vs Receive's 860dp, so Send rendered narrower). + // The recent-sends list below deliberately keeps the full column width (formAvailW). + Layout::CardBox formBox = Layout::mainComposeCardBox(formAvailW); + float formCardW = formBox.width; + float formOffsetX = formBox.offsetX; float formW = formAvailW; ImGui::BeginGroup(); @@ -1367,7 +1368,10 @@ void RenderSendTab(App* app) float pasteW = std::max(schema::UI().drawElement("tabs.send", "paste-btn-min-width").size, colW * schema::UI().drawElement("tabs.send", "paste-btn-width-ratio").size); float contactsW = ImGui::GetFrameHeight(); // compact square icon button for the contact picker - ImGui::PushItemWidth(colW - pasteW - contactsW - Layout::spacingSm() * 2.0f); + // Reserve the TWO real SameLine gaps (each = ItemSpacing.x) between input|Paste|icon. + // Reserving spacingSm (a smaller token) under-counted the gap, so the row overshot colW by + // ~2*(ItemSpacing.x - spacingSm) and the icon's right border clipped past the card edge. + ImGui::PushItemWidth(colW - pasteW - contactsW - ImGui::GetStyle().ItemSpacing.x * 2.0f); // Show clipboard preview as transparent overlay when paste button is hovered bool paste_hovered = false; @@ -1425,9 +1429,12 @@ void RenderSendTab(App* app) } } - // Contact picker — pick a saved contact's address as the recipient. + // Contact picker — pick a saved contact's address as the recipient. Pin the height to the + // frame height (== the input + Paste height) so the larger iconMed font doesn't auto-size + // this square button taller than its row-mates. (Passing a non-zero size also routes + // TactileButton through its precise InvisibleButton + centered-glyph path.) ImGui::SameLine(); - if (material::TactileButton(ICON_MD_CONTACTS "##pickContact", ImVec2(contactsW, 0), + if (material::TactileButton(ICON_MD_CONTACTS "##pickContact", ImVec2(contactsW, ImGui::GetFrameHeight()), material::Type().iconMed())) ImGui::OpenPopup("##ContactPickerPopup"); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("send_contacts_button"));