refactor(ui): extract shared ProgressBar / CopyableTextOverlay / DrawPill helpers

UI-standardization audit: three draw patterns were copy-pasted across tabs and
dialogs. Promote each to a material:: helper and adopt at every verbatim site.
Output is pixel-identical (same colors/geometry threaded through as params).

- material::DrawProgressBar / ProgressBar — replaces 3 byte-identical rounded
  fill bars in the daemon/xmrig/bootstrap download dialogs.
- material::CopyableTextOverlay — the click-to-copy affordance (hit button +
  hand cursor + tooltip + hover underline + clipboard) shared by the peers
  best-block hash and the 3 mining stats (difficulty/block/address). Returns
  "copied this frame" so callers keep their own toast (no ui-layer dependency).
- material::DrawPill / PillSize — rounded bg + optional border + inset text
  badge, shared by the explorer status pill, the market Z/T chip, and the
  network official/custom tag.

Divergent one-offs left in place (transactions status pills use schema rounding
around pre-positioned multi-line text; the mining filter-pill is a button bg).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-04 18:48:08 -05:00
parent 07f6d2efc9
commit ad59d62b82
9 changed files with 102 additions and 94 deletions

View File

@@ -279,6 +279,81 @@ inline ImFont* resolveButtonFont(int perButton, int sectionDefault)
return resolveButtonFont(perButton >= 0 ? perButton : sectionDefault);
}
// ── Progress / fill bar ─────────────────────────────────────────────────
// A rounded track with a fraction (0..1) fill, drawn at the current cursor and advancing the layout
// cursor past it (matches the download-dialog usage). For a placement you control, use the pMin/pMax
// overload. Defaults: theme Primary fill, a subtle white track, 4dp corners.
inline void DrawProgressBar(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax, float frac01,
ImU32 fill = 0, ImU32 track = IM_COL32(255, 255, 255, 30), float rounding = -1.0f)
{
if (fill == 0) fill = Primary();
if (rounding < 0.0f) rounding = 4.0f * Layout::dpiScale();
frac01 = frac01 < 0.0f ? 0.0f : (frac01 > 1.0f ? 1.0f : frac01);
dl->AddRectFilled(pMin, pMax, track, rounding);
if (frac01 > 0.0f)
dl->AddRectFilled(pMin, ImVec2(pMin.x + (pMax.x - pMin.x) * frac01, pMax.y), fill, rounding);
}
inline void ProgressBar(float width, float height, float frac01,
ImU32 fill = 0, ImU32 track = IM_COL32(255, 255, 255, 30))
{
ImVec2 pMin = ImGui::GetCursorScreenPos();
DrawProgressBar(ImGui::GetWindowDrawList(), pMin, ImVec2(pMin.x + width, pMin.y + height), frac01, fill, track);
ImGui::Dummy(ImVec2(0, height));
}
// ── Click-to-copy affordance ────────────────────────────────────────────
// Turns an already-drawn text run into a click-to-copy control. The caller draws the visible text
// (font/color/position of its choosing); this places an invisible hit button (textSize + hitPad) at
// `pos`, shows a hand cursor + tooltip + a hover underline, and copies `copyValue` on click. Returns
// true the frame it was clicked (already on the clipboard) so the caller can show its own toast.
// Caller manages any cursor save/restore around the call.
inline bool CopyableTextOverlay(const char* id, const ImVec2& pos, const ImVec2& textSize, float fontSize,
const char* copyValue, const char* tooltip, const ImVec2& hitPad)
{
const float dp = Layout::dpiScale();
ImGui::SetCursorScreenPos(pos);
ImGui::InvisibleButton(id, ImVec2(textSize.x + hitPad.x, fontSize + hitPad.y));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (tooltip && tooltip[0]) Tooltip("%s", tooltip);
const float uy = pos.y + fontSize + 1.0f * dp;
ImGui::GetWindowDrawList()->AddLine(ImVec2(pos.x, uy), ImVec2(pos.x + textSize.x, uy),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
bool copied = false;
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(copyValue);
copied = true;
}
return copied;
}
// ── Pill / badge ─────────────────────────────────────────────────────────
// A rounded chip: a filled background sized to `text` + `pad`, an optional 1px border, and the text
// inset by `pad`. Use PillSize first when you need to right/center-anchor (compute the width, place
// the pill, then draw).
inline ImVec2 PillSize(const char* text, ImFont* font, const ImVec2& pad)
{
ImVec2 t = font->CalcTextSizeA(font->LegacySize, FLT_MAX, 0.0f, text);
return ImVec2(t.x + pad.x * 2.0f, font->LegacySize + pad.y * 2.0f);
}
// Draw a pill/badge anchored at top-left `pos`. `border` == 0 draws no outline. rounding < 0 => fully
// rounded (height/2). Returns the pill's total size.
inline ImVec2 DrawPill(ImDrawList* dl, const ImVec2& pos, const char* text, ImFont* font,
ImU32 fg, ImU32 bg, ImU32 border = 0,
const ImVec2& pad = ImVec2(4.0f, 2.0f), float rounding = -1.0f)
{
ImVec2 sz = PillSize(text, font, pad);
if (rounding < 0.0f) rounding = sz.y * 0.5f;
ImVec2 pMax(pos.x + sz.x, pos.y + sz.y);
dl->AddRectFilled(pos, pMax, bg, rounding);
if (border != 0) dl->AddRect(pos, pMax, border, rounding, 0, 1.0f * Layout::dpiScale());
dl->AddText(font, font->LegacySize, ImVec2(pos.x + pad.x, pos.y + pad.y), fg, text);
return sz;
}
// ── Tactile wrappers ────────────────────────────────────────────────────
// Drop-in replacements for ImGui::Button / SmallButton that automatically
// add the tactile highlight overlay after rendering.

View File

@@ -162,18 +162,7 @@ private:
ImGui::Spacing();
// Progress bar
float barH = 8.0f * dp;
float barW = ImGui::GetContentRegionAvail().x;
ImVec2 barMin = ImGui::GetCursorScreenPos();
ImVec2 barMax(barMin.x + barW, barMin.y + barH);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(barMin, barMax, IM_COL32(255, 255, 255, 30), 4.0f * dp);
float fillW = barW * (prog.percent / 100.0f);
if (fillW > 0) {
dl->AddRectFilled(barMin, ImVec2(barMin.x + fillW, barMax.y),
Primary(), 4.0f * dp);
}
ImGui::Dummy(ImVec2(0, barH));
material::ProgressBar(ImGui::GetContentRegionAvail().x, 8.0f * dp, prog.percent / 100.0f);
ImGui::Spacing();
// Percent + status text

View File

@@ -189,16 +189,7 @@ private:
: TR("daemon_update_installing");
Type().text(TypeStyle::Subtitle2, title);
ImGui::Spacing();
const float barH = 8.0f * dp;
const float barW = fullW();
const ImVec2 bMin = ImGui::GetCursorScreenPos();
const ImVec2 bMax(bMin.x + barW, bMin.y + barH);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(bMin, bMax, IM_COL32(255, 255, 255, 30), 4.0f * dp);
const float fillW = barW * (p.percent / 100.0f);
if (fillW > 0)
dl->AddRectFilled(bMin, ImVec2(bMin.x + fillW, bMax.y), Primary(), 4.0f * dp);
ImGui::Dummy(ImVec2(0, barH));
material::ProgressBar(fullW(), 8.0f * dp, p.percent / 100.0f);
ImGui::Spacing();
ImGui::Text("%s", p.status_text.c_str());
ImGui::Spacing();

View File

@@ -600,17 +600,11 @@ static void renderChainStats(App* app, float availWidth) {
float maxTextW = cardW * 0.34f;
statusText = truncateHashToFit(statusText, capFont, maxTextW);
ImVec2 textSz = capFont->CalcTextSizeA(capFont->LegacySize, 1000.0f, 0.0f, statusText.c_str());
float pillPadX = Layout::spacingSm();
float pillH = capFont->LegacySize + Layout::spacingXs() * 2.0f;
float pillW = textSz.x + pillPadX * 2.0f;
ImVec2 pillMin(cardMin.x + cardW - pad - pillW, cardMin.y + pad * 0.5f);
ImVec2 pillMax(pillMin.x + pillW, pillMin.y + pillH);
dl->AddRectFilled(pillMin, pillMax, WithAlpha(pillCol, 32), pillH * 0.5f);
dl->AddRect(pillMin, pillMax, WithAlpha(pillCol, 110), pillH * 0.5f, 0, 1.0f * dp);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(pillMin.x + pillPadX, pillMin.y + Layout::spacingXs()), pillCol, statusText.c_str());
ImVec2 pillPad(Layout::spacingSm(), Layout::spacingXs());
ImVec2 pillSz = material::PillSize(statusText.c_str(), capFont, pillPad);
ImVec2 pillMin(cardMin.x + cardW - pad - pillSz.x, cardMin.y + pad * 0.5f);
material::DrawPill(dl, pillMin, statusText.c_str(), capFont, pillCol,
WithAlpha(pillCol, 32), WithAlpha(pillCol, 110), pillPad);
};
auto drawChainTip = [&](const ImVec2& cardMin, float cardW) {

View File

@@ -654,10 +654,9 @@ static void pfDrawAddressSection(App* app)
bool isZ = (a.type == "shielded");
const char* chip = isZ ? "Z" : "T";
ImU32 chipCol = isZ ? Success() : Warning();
float chipW = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, chip).x + 8.0f * dp;
ldl->AddRectFilled(ImVec2(rx, rcy - capF->LegacySize * 0.5f - 2.0f),
ImVec2(rx + chipW, rcy + capF->LegacySize * 0.5f + 2.0f), WithAlpha(chipCol, 40), 4.0f * dp);
ldl->AddText(capF, capF->LegacySize, ImVec2(rx + 4.0f * dp, rcy - capF->LegacySize * 0.5f), chipCol, chip);
ImVec2 chipPos(rx, rcy - capF->LegacySize * 0.5f - 2.0f);
float chipW = material::DrawPill(ldl, chipPos, chip, capF, chipCol,
WithAlpha(chipCol, 40), 0, ImVec2(4.0f * dp, 2.0f), 4.0f * dp).x;
rx += chipW + Layout::spacingSm();
std::string aicon = app->getAddressIcon(a.address);
if (!aicon.empty()) {

View File

@@ -283,17 +283,9 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
dl->AddText(capFont, capFont->LegacySize, ImVec2(col1X + valOffX, cy), OnSurface(), buf);
ImVec2 diffSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
ImGui::SetCursorScreenPos(ImVec2(col1X + valOffX, cy));
ImGui::InvisibleButton("##DiffCopy", ImVec2(diffSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("mining_click_copy_difficulty"));
dl->AddLine(ImVec2(col1X + valOffX, cy + capFont->LegacySize + 1 * dp),
ImVec2(col1X + valOffX + diffSz.x, cy + capFont->LegacySize + 1 * dp),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(buf);
if (material::CopyableTextOverlay("##DiffCopy", ImVec2(col1X + valOffX, cy), diffSz,
capFont->LegacySize, buf, TR("mining_click_copy_difficulty"),
ImVec2(Layout::spacingMd(), 4 * dp))) {
Notifications::instance().info(TR("mining_difficulty_copied"));
}
}
@@ -304,17 +296,9 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
snprintf(buf, sizeof(buf), "%d", mining.blocks);
dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X + valOffX, cy), OnSurface(), buf);
ImVec2 blkSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
ImGui::SetCursorScreenPos(ImVec2(col2X + valOffX, cy));
ImGui::InvisibleButton("##BlockCopy", ImVec2(blkSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("mining_click_copy_block"));
dl->AddLine(ImVec2(col2X + valOffX, cy + capFont->LegacySize + 1 * dp),
ImVec2(col2X + valOffX + blkSz.x, cy + capFont->LegacySize + 1 * dp),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(buf);
if (material::CopyableTextOverlay("##BlockCopy", ImVec2(col2X + valOffX, cy), blkSz,
capFont->LegacySize, buf, TR("mining_click_copy_block"),
ImVec2(Layout::spacingMd(), 4 * dp))) {
Notifications::instance().info(TR("mining_block_copied"));
}
}
@@ -342,17 +326,10 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X + valOffX, cy), OnSurface(), truncAddr.c_str());
ImVec2 addrTextSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, truncAddr.c_str());
ImGui::SetCursorScreenPos(ImVec2(col3X + valOffX, cy));
ImGui::InvisibleButton("##MiningAddrCopy", ImVec2(addrTextSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("mining_click_copy_address"));
dl->AddLine(ImVec2(col3X + valOffX, cy + capFont->LegacySize + 1 * dp),
ImVec2(col3X + valOffX + addrTextSz.x, cy + capFont->LegacySize + 1 * dp),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(mining_address.c_str());
if (material::CopyableTextOverlay("##MiningAddrCopy", ImVec2(col3X + valOffX, cy), addrTextSz,
capFont->LegacySize, mining_address.c_str(),
TR("mining_click_copy_address"),
ImVec2(Layout::spacingMd(), 4 * dp))) {
Notifications::instance().info(TR("mining_address_copied"));
}
}

View File

@@ -285,9 +285,8 @@ void RenderLiteNetworkTab(App* app)
ImVec2 tagSz = cap->CalcTextSizeA(cap->LegacySize, FLT_MAX, 0, tag);
float tagX = rx - tagSz.x;
float tagY = cardMin.y + 9.0f * dp;
dl->AddRectFilled(ImVec2(tagX - 6 * dp, tagY - 1 * dp), ImVec2(rx + 6 * dp, tagY + tagSz.y + 1 * dp),
tagBg, 4.0f * dp);
dl->AddText(cap, cap->LegacySize, ImVec2(tagX, tagY), tagFg, tag);
material::DrawPill(dl, ImVec2(tagX - 6 * dp, tagY - 1 * dp), tag, cap, tagFg, tagBg, 0,
ImVec2(6 * dp, 1 * dp), 4.0f * dp);
if (selected) {
const char* inUse = TR("lite_net_in_use");
ImVec2 uSz = cap->CalcTextSizeA(cap->LegacySize, FLT_MAX, 0, inUse);

View File

@@ -314,17 +314,10 @@ void RenderPeersTab(App* app)
// Click to copy full hash
ImVec2 hashSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, truncHash.c_str());
ImVec2 savedCursor = ImGui::GetCursorScreenPos();
ImGui::SetCursorScreenPos(ImVec2(cx, valY));
ImGui::InvisibleButton("##BestBlockCopy", ImVec2(hashSz.x + Layout::spacingSm(), sub1->LegacySize + 2 * dp));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s %s", TR("peers_click_copy"), hash.c_str());
dl->AddLine(ImVec2(cx, valY + sub1->LegacySize + 1 * dp),
ImVec2(cx + hashSz.x, valY + sub1->LegacySize + 1 * dp),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(hash.c_str());
std::string copyTip = std::string(TR("peers_click_copy")) + " " + hash;
if (material::CopyableTextOverlay("##BestBlockCopy", ImVec2(cx, valY), hashSz,
sub1->LegacySize, hash.c_str(), copyTip.c_str(),
ImVec2(Layout::spacingSm(), 2 * dp))) {
ui::Notifications::instance().info(TR("peers_hash_copied"));
}
ImGui::SetCursorScreenPos(savedCursor);

View File

@@ -182,16 +182,7 @@ private:
: TR("xmrig_installing");
Type().text(TypeStyle::Subtitle2, title);
ImGui::Spacing();
const float barH = 8.0f * dp;
const float barW = fullW();
const ImVec2 bMin = ImGui::GetCursorScreenPos();
const ImVec2 bMax(bMin.x + barW, bMin.y + barH);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(bMin, bMax, IM_COL32(255, 255, 255, 30), 4.0f * dp);
const float fillW = barW * (p.percent / 100.0f);
if (fillW > 0)
dl->AddRectFilled(bMin, ImVec2(bMin.x + fillW, bMax.y), Primary(), 4.0f * dp);
ImGui::Dummy(ImVec2(0, barH));
material::ProgressBar(fullW(), 8.0f * dp, p.percent / 100.0f);
ImGui::Spacing();
ImGui::Text("%s", p.status_text.c_str());
ImGui::Spacing();