fix(send/receive): unify card width, justify receive footer, fix recipient-row button height/clip/glyph
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
// [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(); }
|
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 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); }
|
inline float kTableHeightRatio() { return schema::UI().drawElement("panels", "table").getFloat("height-ratio", 0.45f); }
|
||||||
|
|
||||||
|
|||||||
@@ -544,11 +544,16 @@ inline bool TactileButton(const char* label, const ImVec2& size = ImVec2(0, 0),
|
|||||||
ImVec2 bMin = ImGui::GetItemRectMin();
|
ImVec2 bMin = ImGui::GetItemRectMin();
|
||||||
ImVec2 bMax = ImGui::GetItemRectMax();
|
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) {
|
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);
|
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;
|
float rounding = ImGui::GetStyle().FrameRounding;
|
||||||
|
|||||||
@@ -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
|
// Build preview string
|
||||||
if (!app->isConnected()) {
|
if (!app->isConnected()) {
|
||||||
s_source_preview = TR(app->isLiteBuild() ? "lite_no_wallet_short" : "not_connected");
|
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];
|
const auto& addr = state.addresses[s_selected_address_idx];
|
||||||
bool isZ = addr.type == "shielded";
|
bool isZ = addr.type == "shielded";
|
||||||
const char* tag = isZ ? "[Z]" : "[T]";
|
const char* tag = isZ ? "[Z]" : "[T]";
|
||||||
std::string trunc = util::truncateMiddle(addr.address,
|
// Reserve pixel room for the tag prefix and the trailing balance, then middle-truncate the
|
||||||
static_cast<int>(std::max(schema::UI().drawElement("tabs.receive", "addr-preview-trunc-min").size, width / schema::UI().drawElement("tabs.receive", "addr-preview-trunc-divisor").size)));
|
// address to whatever remains — measured with the combo's own Body2 font. Char-count
|
||||||
snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s",
|
// truncation kept MORE chars as the column widened, so at 150% the scaled font overflowed
|
||||||
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
|
// 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;
|
s_source_preview = buf;
|
||||||
} else {
|
} else {
|
||||||
s_source_preview = TR("select_receiving_address");
|
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::SetNextItemWidth(dropdownW);
|
||||||
ImGui::PushFont(Type().getFont(TypeStyle::Body2));
|
ImGui::PushFont(Type().getFont(TypeStyle::Body2));
|
||||||
if (ImGui::BeginCombo("##RecvAddr", s_source_preview.c_str())) {
|
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
|
// 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).
|
// The RECENT RECEIVED list below stays on the uncapped formW (handled separately).
|
||||||
float cardDp = Layout::dpiScale();
|
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 cardLeftX = ImGui::GetCursorScreenPos().x;
|
||||||
float cardOffsetX = std::max(0.0f, (formW - cardW) * 0.5f);
|
float cardOffsetX = cardBox.offsetX;
|
||||||
if (cardOffsetX > 0.0f)
|
if (cardOffsetX > 0.0f)
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + cardOffsetX);
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + cardOffsetX);
|
||||||
|
|
||||||
@@ -911,7 +927,13 @@ void RenderReceiveTab(App* app)
|
|||||||
{
|
{
|
||||||
float btnGap = Layout::spacingMd();
|
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 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;
|
bool firstBtn = true;
|
||||||
|
|
||||||
|
|||||||
@@ -1272,12 +1272,13 @@ void RenderSendTab(App* app)
|
|||||||
float contentStartY = ImGui::GetCursorPosY();
|
float contentStartY = ImGui::GetCursorPosY();
|
||||||
|
|
||||||
float formAvailW = ImGui::GetContentRegionAvail().x;
|
float formAvailW = ImGui::GetContentRegionAvail().x;
|
||||||
// The compose form reads best as a centered fixed-width column, not edge-to-edge.
|
// Fill the available column up to the content-max-width cap, then center. Shared with the
|
||||||
// Cap the card to a readable form width and center it; the recent-sends list below
|
// Receive tab via mainComposeCardBox() so the two card envelopes are identical in width and
|
||||||
// deliberately keeps the full column width (formAvailW).
|
// position (Send previously capped at 760dp vs Receive's 860dp, so Send rendered narrower).
|
||||||
const float sendDp = Layout::dpiScale();
|
// The recent-sends list below deliberately keeps the full column width (formAvailW).
|
||||||
float formCardW = std::min(formAvailW, 760.0f * sendDp);
|
Layout::CardBox formBox = Layout::mainComposeCardBox(formAvailW);
|
||||||
float formOffsetX = std::max(0.0f, (formAvailW - formCardW) * 0.5f);
|
float formCardW = formBox.width;
|
||||||
|
float formOffsetX = formBox.offsetX;
|
||||||
float formW = formAvailW;
|
float formW = formAvailW;
|
||||||
ImGui::BeginGroup();
|
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 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
|
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
|
// Show clipboard preview as transparent overlay when paste button is hovered
|
||||||
bool paste_hovered = false;
|
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();
|
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()))
|
material::Type().iconMed()))
|
||||||
ImGui::OpenPopup("##ContactPickerPopup");
|
ImGui::OpenPopup("##ContactPickerPopup");
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("send_contacts_button"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("send_contacts_button"));
|
||||||
|
|||||||
Reference in New Issue
Block a user