fix(ui): prevent text/button cutoff and scale hand-drawn geometry at HiDPI
Findings from a UI-cutoff audit — each is a spot where an in-tree helper (truncateMiddle / TruncateToWidth / measured button width / the *dpiScale/*hs factors) was bypassed: - notifications: the toast-pill height/padding/icon-gap were raw logical px while the icon/text drawn inside are DPI-baked, so they clipped the pill at HiDPI. Scale the geometry by dpiScale (not the already-scaled glyph metrics). - settings: in the two-column NODE & SECURITY layout the data-directory path could overrun into the Daemon-binary column (shared draw list, no clip rect between them). Middle-ellipsize it to the column width; the full path stays in the tooltip + click-to-open + copy. - send: the "Confirm & Send" button width came straight from the schema and was never measured against the label, clipping the Russian translation on the pre-broadcast dialog. Size to max(schema width, measured label + padding). - balance: the recent-tx address-column offset missed the `* hs` DPI factor its sibling (amount-right-margin) uses, overlapping the type label at HiDPI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,19 +32,22 @@ void Notifications::render()
|
|||||||
return v >= 0 ? v : fb;
|
return v >= 0 ? v : fb;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Status bar geometry
|
// Status bar geometry. These are logical-px schema values; the icon/text drawn into the pill
|
||||||
float sbHeight = S.window("components.status-bar").height;
|
// are DPI-baked, so scale the box by dpiScale to match the (also DPI-scaled) rendered status bar
|
||||||
if (sbHeight <= 0.0f) sbHeight = 30.0f;
|
// and keep the icon/text inside the pill at HiDPI.
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
|
float sbHeight = S.window("components.status-bar").height * dp;
|
||||||
|
if (sbHeight <= 0.0f) sbHeight = 30.0f * dp;
|
||||||
|
|
||||||
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||||
float viewBottom = viewport->WorkPos.y + viewport->WorkSize.y;
|
float viewBottom = viewport->WorkPos.y + viewport->WorkSize.y;
|
||||||
float viewCenterX = viewport->WorkPos.x + viewport->WorkSize.x * 0.5f;
|
float viewCenterX = viewport->WorkPos.x + viewport->WorkSize.x * 0.5f;
|
||||||
|
|
||||||
// Toast pill sizing — fit inside status bar with margin
|
// Toast pill sizing — fit inside status bar with margin
|
||||||
float pillMarginY = nde("pill-margin-y", 3.0f);
|
float pillMarginY = nde("pill-margin-y", 3.0f) * dp;
|
||||||
float pillHeight = sbHeight - pillMarginY * 2.0f;
|
float pillHeight = sbHeight - pillMarginY * 2.0f;
|
||||||
float pillPadX = nde("padding-x", 12.0f);
|
float pillPadX = nde("padding-x", 12.0f) * dp;
|
||||||
float pillRounding = nde("pill-rounding", 12.0f);
|
float pillRounding = nde("pill-rounding", 12.0f) * dp;
|
||||||
|
|
||||||
// Get accent color based on type — resolved from theme palette
|
// Get accent color based on type — resolved from theme palette
|
||||||
ImVec4 accent_color, text_color;
|
ImVec4 accent_color, text_color;
|
||||||
@@ -89,7 +92,7 @@ void Notifications::render()
|
|||||||
ImFont* textFont = material::Type().caption();
|
ImFont* textFont = material::Type().caption();
|
||||||
ImFont* iconFont = material::Type().iconSmall();
|
ImFont* iconFont = material::Type().iconSmall();
|
||||||
float iconW = iconFont ? iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0.0f, icon).x : 0.0f;
|
float iconW = iconFont ? iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0.0f, icon).x : 0.0f;
|
||||||
float iconGap = 4.0f;
|
float iconGap = 4.0f * dp;
|
||||||
float msgW = textFont ? textFont->CalcTextSizeA(textFont->LegacySize, FLT_MAX, 0.0f, notif.message.c_str()).x : 100.0f;
|
float msgW = textFont ? textFont->CalcTextSizeA(textFont->LegacySize, FLT_MAX, 0.0f, notif.message.c_str()).x : 100.0f;
|
||||||
float pillWidth = pillPadX + iconW + iconGap + msgW + pillPadX;
|
float pillWidth = pillPadX + iconW + iconGap + msgW + pillPadX;
|
||||||
// Clamp to reasonable bounds
|
// Clamp to reasonable bounds
|
||||||
@@ -122,7 +125,7 @@ void Notifications::render()
|
|||||||
// Progress bar at bottom of pill (accent-colored), clipped to pill rounded
|
// Progress bar at bottom of pill (accent-colored), clipped to pill rounded
|
||||||
// corners. Draw a full-pill-size rounded rect and clip it to just the
|
// corners. Draw a full-pill-size rounded rect and clip it to just the
|
||||||
// bottom-left progress strip so both bottom corners are respected.
|
// bottom-left progress strip so both bottom corners are respected.
|
||||||
float progH = nde("progress-bar-height", 2.0f);
|
float progH = nde("progress-bar-height", 2.0f) * dp;
|
||||||
float progW = pillWidth * (1.0f - progress);
|
float progW = pillWidth * (1.0f - progress);
|
||||||
if (progW > 0.0f) {
|
if (progW > 0.0f) {
|
||||||
ImVec2 clipMin(pillX, pMax.y - progH);
|
ImVec2 clipMin(pillX, pMax.y - progH);
|
||||||
|
|||||||
@@ -2081,12 +2081,22 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::SameLine(0, 0);
|
ImGui::SameLine(0, 0);
|
||||||
ImGui::SetCursorPosX(leftX + labelW);
|
ImGui::SetCursorPosX(leftX + labelW);
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Primary()), "%s", dirPath.c_str());
|
// In the two-column layout this shares one draw list with the Daemon-binary
|
||||||
|
// column (no clip rect between them), so a long OS data-dir path (Windows
|
||||||
|
// AppData / macOS Application Support) would overrun into it. Middle-ellipsize
|
||||||
|
// to the remaining column width (room left for the copy button); the full path
|
||||||
|
// stays available via the tooltip, click-to-open, and the copy button.
|
||||||
|
ImFont* pathFont = ImGui::GetFont();
|
||||||
|
const float pathAvailW = contentW - labelW - Layout::spacingSm()
|
||||||
|
- ImGui::GetFrameHeight() - Layout::spacingXs();
|
||||||
|
const std::string dirShown =
|
||||||
|
material::TruncateToWidth(dirPath, pathFont, pathFont->LegacySize, pathAvailW);
|
||||||
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Primary()), "%s", dirShown.c_str());
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
const ImVec2 tmn = ImGui::GetItemRectMin(), tmx = ImGui::GetItemRectMax();
|
const ImVec2 tmn = ImGui::GetItemRectMin(), tmx = ImGui::GetItemRectMax();
|
||||||
dl->AddLine(ImVec2(tmn.x, tmx.y), ImVec2(tmx.x, tmx.y), Primary());
|
dl->AddLine(ImVec2(tmn.x, tmx.y), ImVec2(tmx.x, tmx.y), Primary());
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
material::Tooltip("%s", TR("tt_open_dir"));
|
material::Tooltip("%s\n%s", dirPath.c_str(), TR("tt_open_dir"));
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemClicked()) util::Platform::openFolder(dirPath);
|
if (ImGui::IsItemClicked()) util::Platform::openFolder(dirPath);
|
||||||
ImGui::SameLine(0, Layout::spacingSm());
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
|
|||||||
@@ -820,7 +820,7 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
|
|||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(tx_x, rowPos.y + 2 * dp), OnSurfaceMedium(), display.typeText.c_str());
|
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);
|
float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f) * hs;
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(addrX, rowPos.y + 2 * dp), OnSurfaceDisabled(), display.addressText.c_str());
|
ImVec2(addrX, rowPos.y + 2 * dp), OnSurfaceDisabled(), display.addressText.c_str());
|
||||||
|
|
||||||
|
|||||||
@@ -822,7 +822,17 @@ void RenderSendConfirmPopup(App* app) {
|
|||||||
if (s_sending) {
|
if (s_sending) {
|
||||||
Type().text(TypeStyle::Body2, TR("sending"));
|
Type().text(TypeStyle::Body2, TR("sending"));
|
||||||
} else {
|
} else {
|
||||||
if (TactileButton(TR("confirm_and_send"), ImVec2(S.button("tabs.send", "confirm-button").width * Layout::dpiScale(), std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "confirm-button").font))) {
|
// Size to max(schema width, measured label + padding) so a longer translation (e.g. the
|
||||||
|
// Russian "Подтвердить и отправить") isn't clipped on this pre-broadcast confirm button.
|
||||||
|
ImFont* confirmFont = S.resolveFont(S.button("tabs.send", "confirm-button").font);
|
||||||
|
if (!confirmFont) confirmFont = Type().button();
|
||||||
|
const float confirmW = std::max(
|
||||||
|
S.button("tabs.send", "confirm-button").width * Layout::dpiScale(),
|
||||||
|
confirmFont->CalcTextSizeA(confirmFont->LegacySize, FLT_MAX, 0, TR("confirm_and_send")).x
|
||||||
|
+ ImGui::GetStyle().FramePadding.x * 2.0f + 16.0f * Layout::dpiScale());
|
||||||
|
const float confirmH = std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size,
|
||||||
|
schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs);
|
||||||
|
if (TactileButton(TR("confirm_and_send"), ImVec2(confirmW, confirmH), confirmFont)) {
|
||||||
// Re-validate against LIVE state — the confirm dialog persists across frames, so the
|
// Re-validate against LIVE state — the confirm dialog persists across frames, so the
|
||||||
// balance could have dropped or sync (re)started (or the fee bumped total over available)
|
// balance could have dropped or sync (re)started (or the fee bumped total over available)
|
||||||
// since Review. Don't broadcast a now-invalid transaction.
|
// since Review. Don't broadcast a now-invalid transaction.
|
||||||
|
|||||||
Reference in New Issue
Block a user