diff --git a/src/ui/sidebar.h b/src/ui/sidebar.h index e07ada9..b6d53fa 100644 --- a/src/ui/sidebar.h +++ b/src/ui/sidebar.h @@ -676,17 +676,33 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei ImU32 textCol = selected ? Primary() : (pageNeedsUnlock ? OnSurfaceDisabled() : OnSurfaceMedium()); if (showLabels) { + // Reserve room for a badge (if this item will draw one) so the + // label centers in the space to the left of it instead of + // running underneath the badge circle. + bool itemHasBadge = + (item.page == NavPage::History && status.unconfirmedTxCount > 0) || + (item.page == NavPage::Mining && status.miningActive) || + (item.page == NavPage::Peers && status.peerCount > 0) || + (item.page == NavPage::Chat && status.chatUnreadCount > 0); + float badgeReserve = 0.0f; + if (itemHasBadge) { + bool dotOnlyReserve = (item.page == NavPage::Mining); + float badgeRReserve = dotOnlyReserve ? badgeRadiusDot : badgeRadiusNumber; + float badgeInsetXReserve = sde("badge-inset-x", 6.0f); + badgeReserve = badgeRReserve * 2.0f + badgeInsetXReserve; + } + ImFont* font = selected ? Type().subtitle2() : Type().body2(); float lblFsz = ScaledFontSize(font); float btnW = indMax.x - indMin.x; - float maxLabelW = btnW - iconS * 2.0f - iconLabelGap - Layout::spacingXs() * 2; + float maxLabelW = btnW - iconS * 2.0f - iconLabelGap - Layout::spacingXs() * 2 - badgeReserve; ImVec2 labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item)); if (labelSz.x > maxLabelW && maxLabelW > 0) { lblFsz *= maxLabelW / labelSz.x; labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item)); } float totalW = iconS * 2.0f + iconLabelGap + labelSz.x; - float btnCX = (indMin.x + indMax.x) * 0.5f; + float btnCX = (indMin.x + indMax.x - badgeReserve) * 0.5f; float startX = btnCX - totalW * 0.5f; DrawNavIcon(dl, item.page, startX + iconS, iconCY, iconS, textCol); diff --git a/src/ui/windows/address_label_dialog.h b/src/ui/windows/address_label_dialog.h index 9e56bb5..e8f9a17 100644 --- a/src/ui/windows/address_label_dialog.h +++ b/src/ui/windows/address_label_dialog.h @@ -138,8 +138,13 @@ public: const float controlsTopY = std::max(gridStartY + cellSz * 2.0f, buttonY - preButtonReserve); const float gridMaxH = std::max(cellSz * 2.0f, controlsTopY - gridStartY); ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(0, 0, 0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 11.0f * dp); + ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 5.5f * dp); + // Scrollbar visible (not NoScrollbar) — the icon set exceeds the fixed-height + // grid, so a real scrollbar is the discoverable way to reach the rest. ImGui::BeginChild("##IconGrid", ImVec2(avail, gridMaxH), ImGuiChildFlags_None, - ImGuiWindowFlags_NoScrollbar); + ImGuiWindowFlags_NoScrollWithMouse); + ApplySmoothScroll(); ImDrawList* dl = ImGui::GetWindowDrawList(); @@ -185,6 +190,7 @@ public: } ImGui::EndChild(); + ImGui::PopStyleVar(2); // ScrollbarSize + ScrollbarRounding ImGui::PopStyleColor(); if (ImGui::GetCursorPosY() < controlsTopY) { diff --git a/src/ui/windows/balance_components.cpp b/src/ui/windows/balance_components.cpp index fb607ed..34849fc 100644 --- a/src/ui/windows/balance_components.cpp +++ b/src/ui/windows/balance_components.cpp @@ -805,6 +805,7 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float const float kRecentTxRowHeight = S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f); const auto& state = app->state(); + float headerStartY = ImGui::GetCursorPosY(); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("recent_transactions")); ImGui::SameLine(); @@ -812,6 +813,7 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float app->setCurrentPage(NavPage::History); } ImGui::Spacing(); + float headerHeight = ImGui::GetCursorPosY() - headerStartY; float scaledRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs); float availableListH = ImGui::GetContentRegionAvail().y; @@ -820,14 +822,17 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float ImGuiWindowFlags_NoBackground); const auto& txs = state.transactions; - int count = std::min(4, (int)txs.size()); // show only the 4 most recent (state.transactions is newest-first) + float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs); + // Only draw as many rows as fully fit within the reserved section height (header + rows); + // dropping the overflow row is fine since "View All" already links to full History. + int maxRows = std::max(1, (int)((recentH - headerHeight) / rowH)); + int count = std::min({4, maxRows, (int)txs.size()}); // show only the most recent (state.transactions is newest-first) if (count == 0) { Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions_yet")); } else { ImDrawList* dl = ImGui::GetWindowDrawList(); ImFont* capFont = Type().caption(); - float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs); float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size, S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs); diff --git a/src/ui/windows/console_tab.cpp b/src/ui/windows/console_tab.cpp index 4648df9..d6246c8 100644 --- a/src/ui/windows/console_tab.cpp +++ b/src/ui/windows/console_tab.cpp @@ -564,12 +564,27 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec) ImGui::SameLine(); } - // Line count - ImGui::TextDisabled(TR("console_line_count"), model_.size()); + // Line count — the least-critical trailing element. When the row is too narrow to fit the + // filter box (at its placeholder-sized minimum) AND its trailing controls, drop the line count + // rather than starve/hide the filter (worst at 1024px). Mirror the reservation formula in + // drawFilterInput(): trailing = 4 frame-height buttons + the group spacers, and the filter's + // hard floor = its placeholder width + frame padding. + { + char lineCountBuf[64]; + snprintf(lineCountBuf, sizeof(lineCountBuf), TR("console_line_count"), model_.size()); + float lineCountW = ImGui::CalcTextSize(lineCountBuf).x + Layout::spacingSm() * 2.0f; // text + its trailing spacer + float trailingW = ImGui::GetFrameHeight() * 4.0f + Layout::spacingSm() * 7.0f; + float filterMinW = ImGui::CalcTextSize(TR("console_filter_hint")).x + + ImGui::GetStyle().FramePadding.x * 2.0f + 8.0f * Layout::dpiScale(); + bool showLineCount = ImGui::GetContentRegionAvail().x >= lineCountW + trailingW + filterMinW; - ImGui::SameLine(); - ImGui::Spacing(); - ImGui::SameLine(); + if (showLineCount) { + ImGui::TextDisabled(TR("console_line_count"), model_.size()); + ImGui::SameLine(); + ImGui::Spacing(); + ImGui::SameLine(); + } + } // Output filter input drawFilterInput(); @@ -695,6 +710,13 @@ void ConsoleTab::drawFilterInput() 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); + // Never shrink below the placeholder — otherwise the hint clips to "Filter outp" (or the box + // vanishes) at narrow widths. Floor = placeholder text + frame padding + a little breathing room. + // (drawToolbar() drops the "NNN lines" count when even this floor won't fit alongside the row's + // trailing controls, so this max() doesn't push the zoom/color buttons off-window.) + float filterMinW = ImGui::CalcTextSize(TR("console_filter_hint")).x + + ImGui::GetStyle().FramePadding.x * 2.0f + 8.0f * Layout::dpiScale(); + filterW = std::max(filterMinW, filterW); ImGui::SetNextItemWidth(filterW); ImGui::InputTextWithHint("##ConsoleFilter", TR("console_filter_hint"), filter_text_, sizeof(filter_text_)); if (filter_text_[0] != '\0') { diff --git a/src/ui/windows/daemon_download_dialog.h b/src/ui/windows/daemon_download_dialog.h index ebfb9ce..3a7d15d 100644 --- a/src/ui/windows/daemon_download_dialog.h +++ b/src/ui/windows/daemon_download_dialog.h @@ -408,7 +408,9 @@ private: // ---- Below the info card (outside the surface): verify note + install button ---- ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x); Type().textColored(TypeStyle::Caption, downgrade ? Warning() : OnSurfaceMedium(), noteStr); + ImGui::PopTextWrapPos(); ImGui::Spacing(); // Install button sized to its text and centered in the pane. const float bw = ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x * 2.0f + Layout::spacingLg(); diff --git a/src/ui/windows/receive_tab.cpp b/src/ui/windows/receive_tab.cpp index 8acecc3..21dc2a7 100644 --- a/src/ui/windows/receive_tab.cpp +++ b/src/ui/windows/receive_tab.cpp @@ -324,20 +324,9 @@ static void RenderAddressDropdown(App* app, float width) { } // ============================================================================ -// Helpers: timeAgo / DrawRecvIcon (local copies — originals are static in send_tab) +// Helpers: DrawRecvIcon (local copy — original is static in send_tab). +// Relative time uses the shared util::formatTimeAgoShort ("14d ago"), matching Overview/Send. // ============================================================================ -static std::string recvTimeAgo(int64_t timestamp) { - if (timestamp <= 0) return ""; - int64_t now = (int64_t)std::time(nullptr); - int64_t diff = now - timestamp; - if (diff < 0) diff = 0; - char buf[32]; - if (diff < 60) { snprintf(buf, sizeof(buf), TR("time_seconds_ago"), (long long)diff); return buf; } - if (diff < 3600) { snprintf(buf, sizeof(buf), TR("time_minutes_ago"), (long long)(diff / 60)); return buf; } - if (diff < 86400) { snprintf(buf, sizeof(buf), TR("time_hours_ago"), (long long)(diff / 3600)); return buf; } - snprintf(buf, sizeof(buf), TR("time_days_ago"), (long long)(diff / 86400)); return buf; -} - static void DrawRecvIcon(ImDrawList* dl, float cx, float cy, float s, ImU32 col) { dl->AddTriangleFilled( ImVec2(cx, cy + s), @@ -404,30 +393,37 @@ static void RenderRecentReceived(const AddressInfo& /* addr */, rowDL->AddText(capFont, capFont->LegacySize, 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) * hs; + // Address — start it AFTER the measured type-label width (not a fixed offset that shrinks below + // the label at narrow widths), mirroring Overview's recent-tx list. The schema offset is a floor. + float typeW = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, typeText).x; + float addrX = txX + std::max(S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f) * hs, + typeW + Layout::spacingSm()); std::string addrDisplay = util::truncateMiddle(tx.address, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f)); rowDL->AddText(capFont, capFont->LegacySize, ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str()); - // Amount (right-aligned, first line) + // Time ago — short "14d ago" form (shared helper, matches Overview/Send). Measure it + // first so the amount can chain its right edge off this width and never overlap. + std::string ago = util::formatTimeAgoShort(tx.timestamp); + ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, ago.c_str()); + float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x; + float agoMargin = S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f) * hs; + rowDL->AddText(capFont, capFont->LegacySize, + ImVec2(rightEdge - agoSz.x - agoMargin, rowPos.y + 2.0f * dp), + OnSurfaceDisabled(), ago.c_str()); + + // Amount (right-aligned, first line) — anchored to the LEFT of the time-ago text + // (measured width + a gap) so the two columns can never collide, whatever the strings. snprintf(buf, sizeof(buf), "+%.4f %s", std::abs(tx.amount), DRAGONX_TICKER); ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, buf); - float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x; - float amtX = rightEdge - amtSz.x - std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size, - S.drawElement("tabs.balance", "amount-right-margin").size * hs); + float amtGap = std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size, + S.drawElement("tabs.balance", "amount-right-margin").size * hs); + float amtRightEdge = rightEdge - agoSz.x - agoMargin - amtGap; + float amtX = amtRightEdge - amtSz.x; rowDL->AddText(capFont, capFont->LegacySize, ImVec2(amtX, rowPos.y + 2.0f * dp), recvCol, buf); - // Time ago - 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) * hs, - rowPos.y + 2.0f * dp), - OnSurfaceDisabled(), ago.c_str()); - // Clickable row — hover highlight + navigate to History float rowW = ImGui::GetContentRegionAvail().x; ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH); diff --git a/src/ui/windows/request_payment_dialog.cpp b/src/ui/windows/request_payment_dialog.cpp index 67dc271..ccaae6e 100644 --- a/src/ui/windows/request_payment_dialog.cpp +++ b/src/ui/windows/request_payment_dialog.cpp @@ -9,6 +9,7 @@ #include "../notifications.h" #include "../schema/ui_schema.h" #include "../widgets/qr_code.h" +#include "../widgets/copy_field.h" #include "../material/draw_helpers.h" #include "imgui.h" @@ -198,33 +199,58 @@ void RequestPaymentDialog::render(App* app) // Payment URI display if (!s_payment_uri.empty()) { - // Use a selectable text area for the URI - char uri_buf[1024]; - strncpy(uri_buf, s_payment_uri.c_str(), sizeof(uri_buf) - 1); - material::LabeledInput(TR("request_payment_uri"), "##URI", uri_buf, sizeof(uri_buf), - -1.0f, nullptr, ImGuiInputTextFlags_ReadOnly); - + ImGui::Text("%s", TR("request_payment_uri")); + // Bordered read-only field: bounds the raw drgx: URI within a box (it horizontal-scrolls if + // long) rather than 4-char-chunking it like an address or letting a plain field overflow. + // The "Copy URI" button below copies the full value. + { + char uriBuf[2048]; + snprintf(uriBuf, sizeof(uriBuf), "%s", s_payment_uri.c_str()); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::InputText("##URI", uriBuf, sizeof(uriBuf), ImGuiInputTextFlags_ReadOnly); + } + ImGui::Spacing(); - - // Copy button - if (material::TactileButton(TR("request_copy_uri"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) { + + // Footer buttons: size each to ITS OWN label (action-button.width is a floor, not the size) + // so "Copy Full Address" never clips its final letter — mirrors qr_popup_dialog's per-label + // sizing. Font metrics are already DPI-scaled, so don't multiply by dpiScale() here. + ImFont* btnFont = S.resolveFont(actionBtn.font); + ImGui::PushFont(btnFont); + const float btnPad = ImGui::GetStyle().FramePadding.x * 2.0f + 12.0f; // small margin + float w_uri = ImGui::CalcTextSize(TR("request_copy_uri")).x + btnPad; + float w_addr = ImGui::CalcTextSize(TR("copy_address")).x + btnPad; + ImGui::PopFont(); + if (w_uri < actionBtn.width) w_uri = actionBtn.width; + if (w_addr < actionBtn.width) w_addr = actionBtn.width; + + // Copy URI button + if (material::TactileButton(TR("request_copy_uri"), ImVec2(w_uri, 0), btnFont)) { ImGui::SetClipboardText(s_payment_uri.c_str()); Notifications::instance().success(TR("request_uri_copied")); } - + ImGui::SameLine(); - - if (material::TactileButton(TR("copy_address"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) { + + if (material::TactileButton(TR("copy_address"), ImVec2(w_addr, 0), btnFont)) { ImGui::SetClipboardText(s_address); Notifications::instance().success(TR("address_copied")); } } ImGui::Spacing(); - - // Close button - if (material::TactileButton(TR("close"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) { - s_open = false; + + // Close button — sized to its own label with the same floor. + { + ImFont* btnFont = S.resolveFont(actionBtn.font); + ImGui::PushFont(btnFont); + const float btnPad = ImGui::GetStyle().FramePadding.x * 2.0f + 12.0f; + float w_close = ImGui::CalcTextSize(TR("close")).x + btnPad; + ImGui::PopFont(); + if (w_close < actionBtn.width) w_close = actionBtn.width; + if (material::TactileButton(TR("close"), ImVec2(w_close, 0), btnFont)) { + s_open = false; + } } material::EndOverlayDialog(); } diff --git a/src/ui/windows/xmrig_download_dialog.h b/src/ui/windows/xmrig_download_dialog.h index 06a7668..4f815a0 100644 --- a/src/ui/windows/xmrig_download_dialog.h +++ b/src/ui/windows/xmrig_download_dialog.h @@ -368,7 +368,9 @@ private: // ---- Below the info card: verify / stop-mining note + install button (centered, text-fit) ---- ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x); Type().textColored(TypeStyle::Caption, (mining || downgrade) ? Warning() : OnSurfaceMedium(), noteStr); + ImGui::PopTextWrapPos(); ImGui::Spacing(); const float bw = ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x * 2.0f + Layout::spacingLg(); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - bw) * 0.5f));