feat(ui): in-app FAQ, DPI-scaling audit fixes, mining/stratum polish + localize strings
Bundles the session's UI work (the touched files carry several of these changes together, so they are committed as one coherent UI batch): - FAQ: new RenderFaqDialog + data-driven faq_content, opened from a status-bar "?" (and the Windows title bar), styled like the Wallets modal with search, Wallet/Daemon tabs, and smooth scroll. - DPI/font-scale audit: multiply hand-drawn absolute geometry by Layout::dpiScale() across ~30 files so nothing renders native-size at HiDPI / font_scale 1.5 (verified with a full sweep at 1.5x). - Mining: chart now fills the horizontal space; thread stepper +/- buttons match the input-box height; move the stratum-host toggle into Node & Security (v1.3.0+). - Settings: fix the auto-shield status text overlapping the grid. - Sidebar: drop the peer-count badge on the Network button. - i18n: wrap 193 hardcoded literals with TR() (keys/translations added in the preceding i18n commit), so the security/PIN/lock flow, seed-backup wizard, and witness-rebuild dialog localize. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -93,7 +93,7 @@ public:
|
||||
// Arrow
|
||||
{
|
||||
float arrowCX = ImGui::GetContentRegionAvail().x * 0.5f;
|
||||
ImGui::SetCursorPosX(arrowCX - 8.0f);
|
||||
ImGui::SetCursorPosX(arrowCX - 8.0f * dp);
|
||||
ImFont* iconFont = Type().iconMed();
|
||||
float fsz = ScaledFontSize(iconFont);
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
@@ -312,7 +312,8 @@ private:
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
float btnW = 120.0f;
|
||||
const float dp = Layout::dpiScale();
|
||||
float btnW = 120.0f * dp;
|
||||
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - btnW) * 0.5f);
|
||||
if (TactileButton(TR("close"), ImVec2(btnW, 0))) {
|
||||
s_open = false;
|
||||
|
||||
@@ -264,7 +264,7 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
} else if (rows.empty()) {
|
||||
float cw = ImGui::GetContentRegionAvail().x;
|
||||
float ch = ImGui::GetContentRegionAvail().y;
|
||||
if (ch < 60) ch = 60;
|
||||
if (ch < 60.0f * dp) ch = 60.0f * dp;
|
||||
const char* emptyMsg = addr_search[0] ? TR("no_addresses_match") : TR("no_addresses_yet");
|
||||
ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg);
|
||||
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
|
||||
|
||||
@@ -302,9 +302,9 @@ static void RenderBalanceClassic(App* app)
|
||||
float cardPadLg = (classicPadOverride >= 0.0f) ? classicPadOverride : Layout::spacingLg();
|
||||
|
||||
// Card height: must fit the Market card's content (overline + price + 24h)
|
||||
const float ovGap = S.drawElement("tabs.balance", "overline-value-gap").sizeOr(6.0f);
|
||||
const float valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f);
|
||||
const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f);
|
||||
const float ovGap = S.drawElement("tabs.balance", "overline-value-gap").sizeOr(6.0f) * dp;
|
||||
const float valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f) * dp;
|
||||
const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f) * dp;
|
||||
float marketContentH = cardPadLg
|
||||
+ ovFont->LegacySize + ovGap
|
||||
+ sub1->LegacySize + 2.0f * dp
|
||||
@@ -323,7 +323,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Helper: draw accent stripe on left edge, clipped to card rounded corners.
|
||||
// We draw a full-size rounded rect (left corners only) and clip it to the
|
||||
// stripe width so the shape itself follows the card rounding.
|
||||
const float accentW = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f);
|
||||
const float accentW = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f) * dp;
|
||||
auto drawAccent = [&](const ImVec2& cMin, const ImVec2& cMax, ImU32 col) {
|
||||
dl->PushClipRect(cMin, ImVec2(cMin.x + accentW, cMax.y), true);
|
||||
dl->AddRectFilled(cMin, cMax, col, cardSpec.rounding,
|
||||
@@ -446,7 +446,7 @@ static void RenderBalanceClassic(App* app)
|
||||
ImU32 bd = WithAlpha(fg, 90);
|
||||
snprintf(buf, sizeof(buf), "%s %s",
|
||||
TR("data_stale_prefix"), timeAgo(state.last_balance_update).c_str());
|
||||
ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd);
|
||||
ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd, ImVec2(4.0f * dp, 2.0f * dp));
|
||||
// Hover → explain what stale means and how old the data actually is.
|
||||
if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + pillSz.x, cy + pillSz.y)))
|
||||
Tooltip("%s", TR("data_stale_tooltip"));
|
||||
@@ -456,7 +456,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Hover glow
|
||||
if (material::IsRectHovered(cMin, cMax)) {
|
||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
|
||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,7 +487,7 @@ static void RenderBalanceClassic(App* app)
|
||||
{
|
||||
float privPct = (s_dispTotal > 1e-9)
|
||||
? (float)(s_dispShielded / s_dispTotal * 100.0) : 0.0f;
|
||||
snprintf(buf, sizeof(buf), "%.0f%% of total · %d Z-addr",
|
||||
snprintf(buf, sizeof(buf), TR("baltab_pct_of_total_zaddr"),
|
||||
privPct, (int)state.z_addresses.size());
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
|
||||
WithAlpha(Success(), 160), buf);
|
||||
@@ -498,8 +498,8 @@ static void RenderBalanceClassic(App* app)
|
||||
snprintf(buf, sizeof(buf), "+%.4f", state.unconfirmed_balance);
|
||||
ImVec2 ts = capFont->CalcTextSizeA(
|
||||
capFont->LegacySize, 10000, 0, buf);
|
||||
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f);
|
||||
float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f);
|
||||
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f) * dp;
|
||||
float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f) * dp;
|
||||
ImVec2 bMin(cMax.x - ts.x - bp * 3,
|
||||
cMin.y + cardPadLg);
|
||||
ImVec2 bMax(cMax.x - bp, bMin.y + ts.y + bp);
|
||||
@@ -513,7 +513,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Hover glow + click to Receive
|
||||
if (material::IsRectHovered(cMin, cMax)) {
|
||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
|
||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0))
|
||||
app->setCurrentPage(NavPage::Receive);
|
||||
@@ -543,7 +543,7 @@ static void RenderBalanceClassic(App* app)
|
||||
OnSurfaceMedium(), DRAGONX_TICKER);
|
||||
cy += sub1->LegacySize + valGap;
|
||||
|
||||
snprintf(buf, sizeof(buf), "%d T-addresses",
|
||||
snprintf(buf, sizeof(buf), TR("baltab_t_addresses_count"),
|
||||
(int)state.t_addresses.size());
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
|
||||
OnSurfaceDisabled(), buf);
|
||||
@@ -551,7 +551,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Hover glow + click to Receive
|
||||
if (material::IsRectHovered(cMin, cMax)) {
|
||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
|
||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0))
|
||||
app->setCurrentPage(NavPage::Receive);
|
||||
@@ -589,7 +589,7 @@ static void RenderBalanceClassic(App* app)
|
||||
[](unsigned char c){ return (char)std::toupper(c); });
|
||||
float textW = std::max(pSz.x + tickGap + usdSz.x,
|
||||
ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, marketLabel.c_str()).x);
|
||||
float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f);
|
||||
float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f) * dp;
|
||||
float sparkLeft = cx + textW + sparkGap;
|
||||
float sparkRight = cMax.x - cardPadLg;
|
||||
|
||||
@@ -611,7 +611,7 @@ static void RenderBalanceClassic(App* app)
|
||||
bool pos = market.change_24h >= 0;
|
||||
ImU32 chgCol = pos ? Success()
|
||||
: Error();
|
||||
snprintf(buf, sizeof(buf), "%s%.1f%% 24h",
|
||||
snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"),
|
||||
pos ? "+" : "", market.change_24h);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cx, cy), chgCol, buf);
|
||||
@@ -633,7 +633,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Hover glow + click to Market
|
||||
if (material::IsRectHovered(cMin, cMax)) {
|
||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
|
||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0))
|
||||
app->setCurrentPage(NavPage::Market);
|
||||
@@ -790,20 +790,20 @@ static void RenderBalanceDonut(App* app) {
|
||||
ImFont* capFont = Type().caption();
|
||||
ImFont* body2 = Type().body2();
|
||||
|
||||
float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f);
|
||||
float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f);
|
||||
float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f);
|
||||
float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f);
|
||||
float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f) * dp;
|
||||
float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f) * dp;
|
||||
float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f) * dp;
|
||||
float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f) * dp;
|
||||
|
||||
// Shielded legend
|
||||
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Success());
|
||||
snprintf(buf, sizeof(buf), "Shielded %.8f", s_dispShielded);
|
||||
snprintf(buf, sizeof(buf), TR("baltab_shielded_amount"), s_dispShielded);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Success(), buf);
|
||||
legendY += capFont->LegacySize + legendLineGap;
|
||||
|
||||
// Transparent legend
|
||||
dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Warning());
|
||||
snprintf(buf, sizeof(buf), "Transparent %.8f", s_dispTransparent);
|
||||
snprintf(buf, sizeof(buf), TR("baltab_transparent_amount"), s_dispTransparent);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Warning(), buf);
|
||||
legendY += capFont->LegacySize + legendSectionGap;
|
||||
|
||||
@@ -811,15 +811,15 @@ static void RenderBalanceDonut(App* app) {
|
||||
const auto& market = state.market;
|
||||
if (market.price_usd > 0) {
|
||||
if (market.price_usd >= 0.01)
|
||||
snprintf(buf, sizeof(buf), "Market: $%.4f", market.price_usd);
|
||||
snprintf(buf, sizeof(buf), TR("baltab_market_price_4dp"), market.price_usd);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "Market: $%.8f", market.price_usd);
|
||||
snprintf(buf, sizeof(buf), TR("baltab_market_price_8dp"), market.price_usd);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
|
||||
OnSurfaceMedium(), buf);
|
||||
legendY += capFont->LegacySize + 4 * dp;
|
||||
|
||||
bool pos = market.change_24h >= 0;
|
||||
snprintf(buf, sizeof(buf), "%s%.1f%% 24h", pos ? "+" : "", market.change_24h);
|
||||
snprintf(buf, sizeof(buf), TR("baltab_pct_change_24h"), pos ? "+" : "", market.change_24h);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY),
|
||||
pos ? Success() : Error(), buf);
|
||||
}
|
||||
@@ -946,7 +946,7 @@ static void RenderBalanceConsolidated(App* app) {
|
||||
float divY = cardMin.y + cardH * S.drawElement("tabs.balance.consolidated", "divider-y-ratio").sizeOr(0.55f);
|
||||
dl->AddLine(ImVec2(cardMin.x + pad, divY), ImVec2(cardMax.x - pad, divY),
|
||||
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.consolidated", "divider-alpha").sizeOr(20.0f)),
|
||||
S.drawElement("tabs.balance.consolidated", "divider-thickness").sizeOr(1.0f));
|
||||
S.drawElement("tabs.balance.consolidated", "divider-thickness").sizeOr(1.0f) * dp);
|
||||
|
||||
// Bottom half: proportion bars
|
||||
float barY = divY + Layout::spacingSm();
|
||||
@@ -1122,7 +1122,7 @@ static void RenderBalanceDashboard(App* app) {
|
||||
|
||||
// Accent stripe — clipped to tile rounded corners
|
||||
{
|
||||
float aw = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f);
|
||||
float aw = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f) * dp;
|
||||
dl->PushClipRect(tMin, ImVec2(tMin.x + aw, tMax.y), true);
|
||||
dl->AddRectFilled(tMin, tMax, tiles[i].accent, tileSpec.rounding,
|
||||
ImDrawFlags_RoundCornersLeft);
|
||||
@@ -1159,7 +1159,7 @@ static void RenderBalanceDashboard(App* app) {
|
||||
// Click
|
||||
if (material::IsRectHovered(tMin, tMax)) {
|
||||
dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
||||
tileSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f));
|
||||
tileSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0))
|
||||
app->setCurrentPage(tiles[i].nav);
|
||||
@@ -1208,7 +1208,7 @@ static void RenderBalanceVerticalStack(App* app) {
|
||||
// Font-content floor per row: icon + label + value must fit
|
||||
float vstackRowFontFloor = std::max(body2->LegacySize, capFont->LegacySize)
|
||||
+ Layout::spacingSm() * 2;
|
||||
float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f);
|
||||
float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f) * dp;
|
||||
float vstackFontFloor = vstackRowFontFloor * 4 + rowGap * 3;
|
||||
float vstackCardH = S.drawElement("tabs.balance.vertical-stack", "card-height").size;
|
||||
float stackH;
|
||||
@@ -1238,10 +1238,10 @@ static void RenderBalanceVerticalStack(App* app) {
|
||||
};
|
||||
|
||||
RowInfo rowInfos[4] = {
|
||||
{"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f},
|
||||
{"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio},
|
||||
{"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio},
|
||||
{"Market", ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f},
|
||||
{TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f},
|
||||
{TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio},
|
||||
{TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio},
|
||||
{TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f},
|
||||
};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
@@ -1296,7 +1296,7 @@ static void RenderBalanceVerticalStack(App* app) {
|
||||
// Proportion bar (for shielded/transparent rows — fills gap between label and amount)
|
||||
if (i == 1 || i == 2) {
|
||||
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
|
||||
float barGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f);
|
||||
float barGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f) * dp;
|
||||
float barPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f);
|
||||
float barH = std::max(
|
||||
S.drawElement("tabs.balance.vertical-stack", "bar-min-height").sizeOr(3.0f),
|
||||
@@ -1326,8 +1326,8 @@ static void RenderBalanceVerticalStack(App* app) {
|
||||
// Sparkline in the gap between label and 24h change
|
||||
if (state.market.price_history.size() >= 2) {
|
||||
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label);
|
||||
float sparkGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f);
|
||||
float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f);
|
||||
float sparkGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f) * dp;
|
||||
float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f) * dp;
|
||||
float sparkLeft = px + labelSz.x + sparkGap;
|
||||
float sparkRight = chgX - sparkGap;
|
||||
if (sparkLeft < sparkRight) {
|
||||
@@ -1387,8 +1387,8 @@ static void RenderBalanceVertical2x2(App* app) {
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
// Font-content floor per row: caption text + vertical padding
|
||||
float v2x2RowFontFloor = capFont->LegacySize + Layout::spacingSm() * 2;
|
||||
float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f);
|
||||
float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f);
|
||||
float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f) * dp;
|
||||
float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f) * dp;
|
||||
float v2x2FontFloor = v2x2RowFontFloor * 2 + rowGap;
|
||||
float cardHOverride = S.drawElement(cfgSec, "card-height").size;
|
||||
float stackH;
|
||||
@@ -1431,13 +1431,13 @@ static void RenderBalanceVertical2x2(App* app) {
|
||||
CellInfo cells[2][2] = {
|
||||
// Row 0: Total Balance (left), Shielded (right)
|
||||
{
|
||||
{"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false},
|
||||
{"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
|
||||
{TR("baltab_total_balance"), ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false},
|
||||
{TR("baltab_shielded"), ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true},
|
||||
},
|
||||
// Row 1: Market (left), Transparent (right)
|
||||
{
|
||||
{"Market", ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false},
|
||||
{"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true},
|
||||
{TR("baltab_market"), ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false},
|
||||
{TR("baltab_transparent"), ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1519,8 +1519,8 @@ static void RenderBalanceVertical2x2(App* app) {
|
||||
// Sparkline between label and 24h change
|
||||
if (state.market.price_history.size() >= 2) {
|
||||
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, cell.label);
|
||||
float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f);
|
||||
float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f);
|
||||
float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f) * dp;
|
||||
float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f) * dp;
|
||||
float sparkLeft = px + labelSz.x + sparkGap;
|
||||
float sparkRight = chgX - sparkGap;
|
||||
if (sparkLeft < sparkRight) {
|
||||
@@ -1665,7 +1665,7 @@ static void RenderBalanceShield(App* app) {
|
||||
ImVec2 needleTip(gaugeCx + cosf(needleAngle) * needleLen,
|
||||
gaugeCy + sinf(needleAngle) * needleLen);
|
||||
dl->AddLine(ImVec2(gaugeCx, gaugeCy), needleTip, gaugeCol,
|
||||
S.drawElement("tabs.balance.shield", "needle-thickness").sizeOr(2.0f));
|
||||
S.drawElement("tabs.balance.shield", "needle-thickness").sizeOr(2.0f) * dp);
|
||||
|
||||
// Center text: percentage
|
||||
ImFont* sub1 = Type().subtitle1();
|
||||
@@ -1956,7 +1956,7 @@ static void RenderBalanceTwoRow(App* app) {
|
||||
}
|
||||
|
||||
RenderSyncBar(app, dl, vs);
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.two-row", "sync-gap").sizeOr(2.0f)));
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.two-row", "sync-gap").sizeOr(2.0f) * dp));
|
||||
|
||||
// Row 2: 3 mini-cards inline
|
||||
{
|
||||
@@ -1979,7 +1979,7 @@ static void RenderBalanceTwoRow(App* app) {
|
||||
S.drawElement("tabs.balance.two-row", "mini-rounding-min").sizeOr(4.0f),
|
||||
glassRound * S.drawElement("tabs.balance.two-row", "mini-rounding-ratio").sizeOr(0.5f));
|
||||
ImFont* capFont = Type().caption();
|
||||
float indicatorR = S.drawElement("tabs.balance.two-row", "indicator-radius").sizeOr(3.0f);
|
||||
float indicatorR = S.drawElement("tabs.balance.two-row", "indicator-radius").sizeOr(3.0f) * dp;
|
||||
int balDecimals = (int)S.drawElement("tabs.balance.two-row", "balance-decimals").sizeOr(4.0f);
|
||||
float twoRowPadOverride = S.drawElement("tabs.balance.two-row", "card-padding").size;
|
||||
float miniPad = (twoRowPadOverride >= 0.0f) ? twoRowPadOverride : Layout::spacingSm();
|
||||
@@ -2054,8 +2054,8 @@ static void RenderBalanceTwoRow(App* app) {
|
||||
|
||||
// Sparkline between price and percentage
|
||||
if (market.price_history.size() >= 2) {
|
||||
float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f);
|
||||
float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f);
|
||||
float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f) * dp;
|
||||
float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f) * dp;
|
||||
float sparkLeft = cx + priceSz.x + sparkGap;
|
||||
float sparkRightEdge = sparkRight - sparkGap;
|
||||
if (sparkLeft < sparkRightEdge) {
|
||||
@@ -2153,10 +2153,10 @@ static void RenderBalanceMinimal(App* app) {
|
||||
{
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
ImVec2 sepPos = ImGui::GetCursorScreenPos();
|
||||
float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f);
|
||||
float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.0f);
|
||||
float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f) * dp;
|
||||
float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.0f) * dp;
|
||||
float sepAlpha = S.drawElement("tabs.balance.minimal", "separator-alpha").sizeOr(25.0f);
|
||||
float sepThick = S.drawElement("tabs.balance.minimal", "separator-thickness").sizeOr(1.0f);
|
||||
float sepThick = S.drawElement("tabs.balance.minimal", "separator-thickness").sizeOr(1.0f) * dp;
|
||||
float x = sepPos.x;
|
||||
float endX = sepPos.x + availW;
|
||||
while (x < endX) {
|
||||
|
||||
@@ -59,7 +59,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
|
||||
s_loading = false;
|
||||
|
||||
if (!error.empty()) {
|
||||
s_error = "Error: " + error;
|
||||
s_error = std::string(TR("grpa_error_prefix")) + error;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ static void handleBlockResponseUnified(const json& result, const std::string& er
|
||||
|
||||
s_has_data = true;
|
||||
} else {
|
||||
s_error = "Invalid response from daemon";
|
||||
s_error = TR("grpa_invalid_response_from_daemon");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ void BlockInfoDialog::render(App* app)
|
||||
|
||||
// Current block info
|
||||
if (state.sync.blocks > 0) {
|
||||
ImGui::TextDisabled("(Current: %d)", state.sync.blocks);
|
||||
ImGui::TextDisabled(TR("grpa_current_block_paren"), state.sync.blocks);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
@@ -152,7 +152,7 @@ void BlockInfoDialog::render(App* app)
|
||||
rpc::RPCClient::TraceScope trace("Explorer / Block info");
|
||||
auto hashResult = rpc->call("getblockhash", {height});
|
||||
if (!hashResult.is_string()) {
|
||||
error = "unexpected getblockhash result";
|
||||
error = TR("grpa_unexpected_getblockhash_result");
|
||||
} else {
|
||||
block = rpc->call("getblock", {hashResult.get<std::string>()});
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ private:
|
||||
|
||||
if (!s_bootstrap) {
|
||||
s_state = State::Failed;
|
||||
s_errorMsg = "Bootstrap not initialized";
|
||||
s_errorMsg = TR("grpc_bootstrap_not_initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ private:
|
||||
s_state = State::Done;
|
||||
} else {
|
||||
s_errorMsg = finalProg.error;
|
||||
if (s_errorMsg.empty()) s_errorMsg = "Bootstrap failed";
|
||||
if (s_errorMsg.empty()) s_errorMsg = TR("grpc_bootstrap_failed");
|
||||
s_state = State::Failed;
|
||||
}
|
||||
s_bootstrap.reset();
|
||||
|
||||
@@ -425,9 +425,9 @@ void RenderChatSettingsPreview(App* app, float width) {
|
||||
struct PMsg { const char* body; bool outgoing; bool startGroup; bool lastInGroup; std::string meta; };
|
||||
const std::string peer = "Ava";
|
||||
const PMsg msgs[] = {
|
||||
{ u8"Did the payment go through? \U0001F642", false, true, true, peer + " " + t1 },
|
||||
{ u8"Yep — just confirmed ✅", true, true, false, std::string(TR("chat_you")) + " " + t2 },
|
||||
{ u8"Sending the rest now \U0001F44D", true, false, true, std::string() },
|
||||
{ TR("grpb_preview_msg_payment_through"), false, true, true, peer + " " + t1 },
|
||||
{ TR("grpb_preview_msg_yep_confirmed"), true, true, false, std::string(TR("chat_you")) + " " + t2 },
|
||||
{ TR("grpb_preview_msg_sending_rest"), true, false, true, std::string() },
|
||||
};
|
||||
const int N = 3;
|
||||
|
||||
|
||||
@@ -337,7 +337,7 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
|
||||
float outputH = ComputeConsoleOutputHeight(
|
||||
availHeight,
|
||||
input_height,
|
||||
schema::UI().drawElement("tabs.console", "output-min-height").size,
|
||||
schema::UI().drawElement("tabs.console", "output-min-height").size * Layout::dpiScale(),
|
||||
schema::UI().drawElement("tabs.console", "output-min-height-ratio").size);
|
||||
|
||||
ImDrawList* dlOut = ImGui::GetWindowDrawList();
|
||||
|
||||
@@ -108,13 +108,13 @@ static const char* relativeTime(int64_t timestamp) {
|
||||
int64_t diff = now - timestamp;
|
||||
if (diff < 0) diff = 0;
|
||||
if (diff < 60)
|
||||
snprintf(buf, sizeof(buf), "%lld sec ago", (long long)diff);
|
||||
snprintf(buf, sizeof(buf), TR("grpa_sec_ago"), (long long)diff);
|
||||
else if (diff < 3600)
|
||||
snprintf(buf, sizeof(buf), "%lld min ago", (long long)(diff / 60));
|
||||
snprintf(buf, sizeof(buf), TR("grpa_min_ago"), (long long)(diff / 60));
|
||||
else if (diff < 86400)
|
||||
snprintf(buf, sizeof(buf), "%lld hr ago", (long long)(diff / 3600));
|
||||
snprintf(buf, sizeof(buf), TR("grpa_hr_ago"), (long long)(diff / 3600));
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "%lld days ago", (long long)(diff / 86400));
|
||||
snprintf(buf, sizeof(buf), TR("grpa_days_ago"), (long long)(diff / 86400));
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -1216,7 +1216,7 @@ static void renderBlockDetailModal(App* app) {
|
||||
txDL->AddRectFilled(rowStart,
|
||||
ImVec2(rowStart.x + txContentW, rowStart.y + txRowH),
|
||||
WithAlpha(OnSurface(), 10),
|
||||
S.drawElement("tabs.explorer", "row-rounding").size);
|
||||
S.drawElement("tabs.explorer", "row-rounding").size * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
material::Tooltip("%s", txid.c_str());
|
||||
}
|
||||
@@ -1315,7 +1315,7 @@ static void renderBlockDetailModal(App* app) {
|
||||
}
|
||||
|
||||
if (s_detail_txids.size() > 100) {
|
||||
snprintf(buf, sizeof(buf), "... showing first 100 of %d", (int)s_detail_txids.size());
|
||||
snprintf(buf, sizeof(buf), TR("grpa_showing_first_100_of"), (int)s_detail_txids.size());
|
||||
ImGui::TextDisabled("%s", buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "faq_content.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
namespace faq {
|
||||
|
||||
// ── Wallet group ───────────────────────────────────────────────────────────
|
||||
const std::vector<FaqSubcategory>& walletFaq()
|
||||
{
|
||||
static const std::vector<FaqSubcategory> kWallet = {
|
||||
{ "faq_w_gs_title", {
|
||||
{ "faq_w_gs_1_q", "faq_w_gs_1_a" },
|
||||
{ "faq_w_gs_2_q", "faq_w_gs_2_a" },
|
||||
{ "faq_w_gs_3_q", "faq_w_gs_3_a" },
|
||||
{ "faq_w_gs_4_q", "faq_w_gs_4_a" },
|
||||
}},
|
||||
{ "faq_w_addr_title", {
|
||||
{ "faq_w_addr_1_q", "faq_w_addr_1_a" },
|
||||
{ "faq_w_addr_2_q", "faq_w_addr_2_a" },
|
||||
{ "faq_w_addr_3_q", "faq_w_addr_3_a" },
|
||||
{ "faq_w_addr_4_q", "faq_w_addr_4_a" },
|
||||
}},
|
||||
{ "faq_w_send_title", {
|
||||
{ "faq_w_send_1_q", "faq_w_send_1_a" },
|
||||
{ "faq_w_send_2_q", "faq_w_send_2_a" },
|
||||
{ "faq_w_send_3_q", "faq_w_send_3_a" },
|
||||
{ "faq_w_send_4_q", "faq_w_send_4_a" },
|
||||
}},
|
||||
{ "faq_w_bal_title", {
|
||||
{ "faq_w_bal_1_q", "faq_w_bal_1_a" },
|
||||
{ "faq_w_bal_2_q", "faq_w_bal_2_a" },
|
||||
{ "faq_w_bal_3_q", "faq_w_bal_3_a" },
|
||||
}},
|
||||
{ "faq_w_sec_title", {
|
||||
{ "faq_w_sec_1_q", "faq_w_sec_1_a" },
|
||||
{ "faq_w_sec_2_q", "faq_w_sec_2_a" },
|
||||
{ "faq_w_sec_3_q", "faq_w_sec_3_a" },
|
||||
}},
|
||||
{ "faq_w_seed_title", {
|
||||
{ "faq_w_seed_1_q", "faq_w_seed_1_a" },
|
||||
{ "faq_w_seed_2_q", "faq_w_seed_2_a" },
|
||||
{ "faq_w_seed_3_q", "faq_w_seed_3_a" },
|
||||
{ "faq_w_seed_4_q", "faq_w_seed_4_a" },
|
||||
}},
|
||||
{ "faq_w_chat_title", {
|
||||
{ "faq_w_chat_1_q", "faq_w_chat_1_a" },
|
||||
{ "faq_w_chat_2_q", "faq_w_chat_2_a" },
|
||||
{ "faq_w_chat_3_q", "faq_w_chat_3_a" },
|
||||
}},
|
||||
{ "faq_w_set_title", {
|
||||
{ "faq_w_set_1_q", "faq_w_set_1_a" },
|
||||
{ "faq_w_set_2_q", "faq_w_set_2_a" },
|
||||
{ "faq_w_set_3_q", "faq_w_set_3_a" },
|
||||
{ "faq_w_set_4_q", "faq_w_set_4_a" },
|
||||
}},
|
||||
};
|
||||
return kWallet;
|
||||
}
|
||||
|
||||
// ── Daemon group (full-node only) ──────────────────────────────────────────
|
||||
const std::vector<FaqSubcategory>& daemonFaq()
|
||||
{
|
||||
static const std::vector<FaqSubcategory> kDaemon = {
|
||||
{ "faq_d_node_title", {
|
||||
{ "faq_d_node_1_q", "faq_d_node_1_a" },
|
||||
{ "faq_d_node_2_q", "faq_d_node_2_a" },
|
||||
{ "faq_d_node_3_q", "faq_d_node_3_a" },
|
||||
}},
|
||||
{ "faq_d_sync_title", {
|
||||
{ "faq_d_sync_1_q", "faq_d_sync_1_a" },
|
||||
{ "faq_d_sync_2_q", "faq_d_sync_2_a" },
|
||||
{ "faq_d_sync_3_q", "faq_d_sync_3_a" },
|
||||
{ "faq_d_sync_4_q", "faq_d_sync_4_a" },
|
||||
}},
|
||||
{ "faq_d_mgmt_title", {
|
||||
{ "faq_d_mgmt_1_q", "faq_d_mgmt_1_a" },
|
||||
{ "faq_d_mgmt_2_q", "faq_d_mgmt_2_a" },
|
||||
{ "faq_d_mgmt_3_q", "faq_d_mgmt_3_a" },
|
||||
}},
|
||||
{ "faq_d_upd_title", {
|
||||
{ "faq_d_upd_1_q", "faq_d_upd_1_a" },
|
||||
{ "faq_d_upd_2_q", "faq_d_upd_2_a" },
|
||||
{ "faq_d_upd_3_q", "faq_d_upd_3_a" },
|
||||
}},
|
||||
{ "faq_d_mine_title", {
|
||||
{ "faq_d_mine_1_q", "faq_d_mine_1_a" },
|
||||
{ "faq_d_mine_2_q", "faq_d_mine_2_a" },
|
||||
{ "faq_d_mine_3_q", "faq_d_mine_3_a" },
|
||||
{ "faq_d_mine_4_q", "faq_d_mine_4_a" },
|
||||
}},
|
||||
{ "faq_d_net_title", {
|
||||
{ "faq_d_net_1_q", "faq_d_net_1_a" },
|
||||
{ "faq_d_net_2_q", "faq_d_net_2_a" },
|
||||
}},
|
||||
{ "faq_d_perf_title", {
|
||||
{ "faq_d_perf_1_q", "faq_d_perf_1_a" },
|
||||
{ "faq_d_perf_2_q", "faq_d_perf_2_a" },
|
||||
}},
|
||||
{ "faq_d_trbl_title", {
|
||||
{ "faq_d_trbl_1_q", "faq_d_trbl_1_a" },
|
||||
{ "faq_d_trbl_2_q", "faq_d_trbl_2_a" },
|
||||
{ "faq_d_trbl_3_q", "faq_d_trbl_3_a" },
|
||||
{ "faq_d_trbl_4_q", "faq_d_trbl_4_a" },
|
||||
}},
|
||||
};
|
||||
return kDaemon;
|
||||
}
|
||||
|
||||
} // namespace faq
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -0,0 +1,41 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// FAQ content model. The FAQ screen is data-driven: this header exposes the two
|
||||
// top-level groups (Wallet, Daemon) as ordered lists of subcategories, each a list
|
||||
// of question/answer entries. Every string is an i18n KEY (looked up with TR at
|
||||
// render time), so wording + translations live in src/util/i18n.cpp + res/lang/*.json
|
||||
// and never require touching UI code. Add a Q&A by appending a {qKey,aKey} pair here
|
||||
// and its two strings to loadBuiltinEnglish().
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
namespace faq {
|
||||
|
||||
// One question and its answer, both i18n keys. The answer may contain "\n\n"
|
||||
// paragraph breaks; it is rendered wrapped. Keep answers free of printf specifiers
|
||||
// (%d/%s/…) — the i18n layer rejects translations whose format signature drifts.
|
||||
struct FaqEntry {
|
||||
const char* questionKey;
|
||||
const char* answerKey;
|
||||
};
|
||||
|
||||
// A named group of questions. titleKey is an i18n key for the subcategory header.
|
||||
struct FaqSubcategory {
|
||||
const char* titleKey;
|
||||
std::vector<FaqEntry> entries;
|
||||
};
|
||||
|
||||
// The two top-level groups. daemonFaq() is full-node material and is only shown when
|
||||
// the build supports full-node lifecycle actions (see App::supportsFullNodeLifecycleActions()).
|
||||
const std::vector<FaqSubcategory>& walletFaq();
|
||||
const std::vector<FaqSubcategory>& daemonFaq();
|
||||
|
||||
} // namespace faq
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -0,0 +1,201 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "faq_dialog.h"
|
||||
#include "faq_content.h"
|
||||
#include "../../app.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/text_format.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../layout.h"
|
||||
#include "../material/type.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "imgui.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
namespace {
|
||||
|
||||
// Persists across frames (the dialog is re-entered each frame while open). Group 0 = Wallet,
|
||||
// 1 = Daemon. `expanded` keys are FaqEntry::questionKey (stable string literals from faq_content).
|
||||
struct FaqDialogState {
|
||||
int group = 0;
|
||||
char search[128] = "";
|
||||
std::unordered_map<std::string, bool> expanded;
|
||||
};
|
||||
FaqDialogState s_faq;
|
||||
|
||||
bool entryMatches(const faq::FaqEntry& e, const char* query)
|
||||
{
|
||||
return util::containsIgnoreCase(TR(e.questionKey), query) ||
|
||||
util::containsIgnoreCase(TR(e.answerKey), query);
|
||||
}
|
||||
|
||||
// One answer body: wrapped, muted. Shared by the collapsible (non-search) and search paths.
|
||||
void renderAnswer(const char* answerKey)
|
||||
{
|
||||
ImGui::Indent(Layout::spacingMd());
|
||||
ImGui::PushFont(material::Type().body2());
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
|
||||
ImGui::TextWrapped("%s", TR(answerKey));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
ImGui::Unindent(Layout::spacingMd());
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RenderFaqDialog(App* app, bool* p_open)
|
||||
{
|
||||
auto& S = schema::UI();
|
||||
auto win = S.window("dialogs.faq");
|
||||
const float dp = Layout::dpiScale();
|
||||
|
||||
const bool daemonAvailable = app && app->supportsFullNodeLifecycleActions();
|
||||
if (!daemonAvailable) s_faq.group = 0; // no Daemon tab in lite builds
|
||||
|
||||
// Floating "BlurFloat" modal, matching the Wallets dialog: live-blur backdrop, no boxed card, a
|
||||
// plain heading (no ✕ — a Close button sits at the bottom). Roomy fixed width; height capped to the
|
||||
// viewport so the content flexes + scrolls on small / HiDPI screens.
|
||||
const float vpH = ImGui::GetMainViewport()->Size.y;
|
||||
const float wantH = (win.height > 0 ? win.height : 640.0f) * dp;
|
||||
material::OverlayDialogSpec spec;
|
||||
spec.title = TR("faq_title");
|
||||
spec.p_open = p_open;
|
||||
spec.style = material::OverlayStyle::BlurFloat;
|
||||
spec.cardWidth = (win.width > 0 ? win.width : 860.0f);
|
||||
spec.cardHeight = std::min(wantH, vpH * 0.86f) / dp;
|
||||
spec.idSuffix = "faq";
|
||||
if (!material::BeginOverlayDialog(spec)) {
|
||||
return;
|
||||
}
|
||||
// Esc closes (ImGui consumes Esc itself while the search box is being edited, so this only
|
||||
// fires when the field isn't capturing it).
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) *p_open = false;
|
||||
|
||||
// Subtitle under the plain heading, matching the Wallets dialog's intro caption.
|
||||
material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(), TR("faq_intro"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
const float contentW = ImGui::GetContentRegionAvail().x;
|
||||
|
||||
// ── Group tabs: Wallet | Daemon ──
|
||||
{
|
||||
const int nTabs = daemonAvailable ? 2 : 1;
|
||||
const float gap = ImGui::GetStyle().ItemSpacing.x;
|
||||
const float tabW = (contentW - gap * (nTabs - 1)) / static_cast<float>(nTabs);
|
||||
auto tab = [&](const char* label, int idx) {
|
||||
const bool active = (s_faq.group == idx);
|
||||
if (active) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnPrimary()));
|
||||
}
|
||||
if (material::TactileButton(label, ImVec2(tabW, 0))) s_faq.group = idx;
|
||||
if (active) ImGui::PopStyleColor(2);
|
||||
};
|
||||
tab(TR("faq_group_wallet"), 0);
|
||||
if (daemonAvailable) { ImGui::SameLine(); tab(TR("faq_group_daemon"), 1); }
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// ── Search ──
|
||||
ImGui::SetNextItemWidth(contentW);
|
||||
ImGui::InputTextWithHint("##FaqSearch", TR("faq_search_hint"), s_faq.search, sizeof(s_faq.search));
|
||||
const bool searching = s_faq.search[0] != '\0';
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// ── Scrollable Q&A body ── (reserve room for the Close button footer below)
|
||||
const float footerH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingMd();
|
||||
float bodyH = ImGui::GetContentRegionAvail().y - footerH;
|
||||
if (bodyH < 80.0f * dp) bodyH = 80.0f * dp;
|
||||
// Inner padding gives the content breathing room and, on the right, a clear gap to the LEFT of the
|
||||
// scrollbar (WindowPadding.x is exactly that gap); the scrollbar itself is made chunkier than the
|
||||
// app default. NoScrollWithMouse + ApplySmoothScroll gives the wheel eased scrolling, matching the
|
||||
// Wallets dialog / Settings page (ApplySmoothScroll owns the wheel input and lerps ScrollY).
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingMd(), Layout::spacingXs()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 16.0f * dp);
|
||||
ImGui::BeginChild("##FaqScroll", ImVec2(0, bodyH), false,
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
material::ApplySmoothScroll();
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
const auto& groups = (s_faq.group == 1 && daemonAvailable) ? faq::daemonFaq() : faq::walletFaq();
|
||||
bool anyShown = false;
|
||||
|
||||
bool firstSection = true;
|
||||
for (const auto& subcat : groups) {
|
||||
// Collect the entries visible under the current search.
|
||||
std::vector<const faq::FaqEntry*> visible;
|
||||
for (const auto& e : subcat.entries) {
|
||||
if (!searching || entryMatches(e, s_faq.search)) visible.push_back(&e);
|
||||
}
|
||||
if (visible.empty()) continue;
|
||||
|
||||
// Section break: generous space above every section after the first, so topic groups read as
|
||||
// clearly separated bands rather than one uniform list.
|
||||
if (!firstSection) ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
|
||||
firstSection = false;
|
||||
anyShown = true;
|
||||
|
||||
// Section header — accent overline + a thin full-width rule beneath it, anchoring the group
|
||||
// above its (brighter, normal-case) question rows.
|
||||
material::Type().textColored(material::TypeStyle::Overline,
|
||||
material::Primary(), TR(subcat.titleKey));
|
||||
{
|
||||
const ImVec2 rp = ImGui::GetCursorScreenPos();
|
||||
const float rw = ImGui::GetContentRegionAvail().x;
|
||||
dl->AddLine(ImVec2(rp.x, rp.y + dp), ImVec2(rp.x + rw, rp.y + dp),
|
||||
material::Divider(), 1.0f * dp);
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
for (const auto* e : visible) {
|
||||
const float rowW = ImGui::GetContentRegionAvail().x;
|
||||
if (searching) {
|
||||
// Search results: show question + answer directly (no collapsing).
|
||||
ImGui::PushFont(material::Type().subtitle2());
|
||||
ImGui::TextWrapped("%s", TR(e->questionKey));
|
||||
ImGui::PopFont();
|
||||
renderAnswer(e->answerKey);
|
||||
} else {
|
||||
bool& exp = s_faq.expanded[e->questionKey];
|
||||
std::string id = std::string("##faq_") + e->questionKey;
|
||||
material::CollapsibleHeader(dl, id.c_str(), TR(e->questionKey), exp, rowW,
|
||||
material::Type().subtitle2(), material::OnSurface());
|
||||
if (exp) renderAnswer(e->answerKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyShown) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
|
||||
ImGui::TextWrapped("%s", TR("faq_no_results"));
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar(2); // ScrollbarSize, WindowPadding
|
||||
|
||||
// Close button footer (BlurFloat has no ✕ in the heading).
|
||||
const float closeW = 120.0f * dp;
|
||||
material::BeginOverlayDialogFooter(closeW, false);
|
||||
if (material::TactileButton(TR("close"), ImVec2(closeW, 0))) *p_open = false;
|
||||
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -0,0 +1,18 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace dragonx {
|
||||
class App;
|
||||
namespace ui {
|
||||
|
||||
// Renders the Help & FAQ modal. Call every frame while *p_open is true; the dialog
|
||||
// clears *p_open itself on close (✕ / click-outside / Esc). Content is data-driven
|
||||
// (see faq_content.h) and grouped into Wallet and Daemon tabs; the Daemon tab is
|
||||
// hidden on builds without full-node lifecycle support.
|
||||
void RenderFaqDialog(App* app, bool* p_open);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -173,7 +173,7 @@ void KeyExportDialog::render(App* app)
|
||||
s_key = found;
|
||||
s_show_key = wantViewing; // viewing keys are less sensitive
|
||||
} else {
|
||||
s_error = r.ok ? std::string("Key not available for this address") : r.error;
|
||||
s_error = r.ok ? std::string(TR("grpc_key_not_available")) : r.error;
|
||||
}
|
||||
wallet::secureWipeLiteSecret(found);
|
||||
s_fetching = false;
|
||||
@@ -240,14 +240,15 @@ void KeyExportDialog::render(App* app)
|
||||
ImGui::TextDisabled("%s", TR("key_export_click_retrieve"));
|
||||
} else {
|
||||
// Key fetched. Layout: [ key text (click-to-copy) + Show/Hide below ] [ QR | square ].
|
||||
const float gap = 12.0f;
|
||||
const float dp = Layout::dpiScale();
|
||||
const float gap = 12.0f * dp;
|
||||
const float avail = ImGui::GetContentRegionAvail().x;
|
||||
// Larger, responsive QR: ~30% of the content width, clamped to a comfortable range.
|
||||
float qrSize = avail * 0.30f;
|
||||
if (qrSize < 200.0f) qrSize = 200.0f;
|
||||
if (qrSize > 340.0f) qrSize = 340.0f;
|
||||
if (qrSize < 200.0f * dp) qrSize = 200.0f * dp;
|
||||
if (qrSize > 340.0f * dp) qrSize = 340.0f * dp;
|
||||
float keyW = avail - qrSize - gap;
|
||||
const bool sideBySide = keyW >= 200.0f; // too narrow -> stack the QR under the key
|
||||
const bool sideBySide = keyW >= 200.0f * dp; // too narrow -> stack the QR under the key
|
||||
if (!sideBySide) keyW = avail;
|
||||
|
||||
// Chunk for readability, but keep a Bech32 HRP (e.g. "secret-extended-key-main") intact
|
||||
|
||||
@@ -1580,7 +1580,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
|
||||
}
|
||||
} else {
|
||||
const char* status = market.price_loading ? TR("market_price_loading") : TR("market_price_unavailable");
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx0, cy + 10), OnSurfaceDisabled(), status);
|
||||
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx0, cy + 10 * dp), OnSurfaceDisabled(), status);
|
||||
if (!market.price_loading && !market.price_error.empty()) {
|
||||
std::string errorText = market.price_error;
|
||||
float maxErrorW = cardMax.x - cx0 - Layout::spacingLg();
|
||||
@@ -1590,7 +1590,7 @@ static void mktDrawPriceHero(const MktCtx& cx)
|
||||
}
|
||||
if (errorText.size() < market.price_error.size()) errorText += "...";
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cx0, cy + 10 + sub1->LegacySize + Layout::spacingXs()),
|
||||
ImVec2(cx0, cy + 10 * dp + sub1->LegacySize + Layout::spacingXs()),
|
||||
Warning(), errorText.c_str());
|
||||
}
|
||||
}
|
||||
@@ -1874,7 +1874,7 @@ static void mktDrawPriceChart(const MktCtx& cx)
|
||||
: (tk == ticks - 1) ? plotRight - lblSz.x
|
||||
: xpos - lblSz.x * 0.5f;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(lx, plotBottom + 4), OnSurfaceDisabled(), tlbl);
|
||||
ImVec2(lx, plotBottom + 4 * mktDp), OnSurfaceDisabled(), tlbl);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,7 +153,12 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
}
|
||||
};
|
||||
ImFont* stepFont = Type().iconSmall();
|
||||
const float fieldH = capFont->LegacySize + 6.0f * dp;
|
||||
// Match the -/+ button height to the InputInt's actual frame height (GetFrameHeight ==
|
||||
// fontSize + 2*FramePadding.y, evaluated with the same font/style the input renders with,
|
||||
// since nothing pushes a font between here and the InputInt below). This keeps the buttons
|
||||
// the SAME height as the number box and top-aligned with it at sy — previously fieldH was
|
||||
// capFont+6px, shorter than the box, so they sat misaligned.
|
||||
const float fieldH = ImGui::GetFrameHeight();
|
||||
const float sideW = fieldH; // square -/+ buttons
|
||||
const float fieldW = 46.0f * dp;
|
||||
const float g = 2.0f * dp;
|
||||
@@ -544,7 +549,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
||||
s_benchConfirm = true;
|
||||
char msg[128];
|
||||
snprintf(msg, sizeof(msg),
|
||||
"Benchmark takes ~%ds and interrupts mining. Click again to start.",
|
||||
TR("grpc_benchmark_takes_secs"),
|
||||
(int)(s_benchmark.totalEstimatedSecs() + 0.5f));
|
||||
Notifications::instance().warning(msg);
|
||||
} else {
|
||||
|
||||
@@ -220,7 +220,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
||||
if (estActive)
|
||||
snprintf(estVal, sizeof(estVal), "~%.4f", estDaily);
|
||||
else
|
||||
snprintf(estVal, sizeof(estVal), "N/A");
|
||||
snprintf(estVal, sizeof(estVal), "%s", TR("grpc_na"));
|
||||
// Disclose in pool mode that Est. Daily is a rough solo-equivalent (before the pool fee), so the
|
||||
// number isn't silently mismatched to its plain "Est. Daily" label. (M-05)
|
||||
const char* estSub = (s_pool_mode && estActive) ? TR("mining_est_daily_pool_sub") : nullptr;
|
||||
@@ -475,7 +475,7 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
||||
else if (totalRAM > 0)
|
||||
snprintf(sysBuf, sizeof(sysBuf), "-- / %.0f GB", totalRAM / 1024.0);
|
||||
else
|
||||
snprintf(sysBuf, sizeof(sysBuf), "N/A");
|
||||
snprintf(sysBuf, sizeof(sysBuf), "%s", TR("grpc_na"));
|
||||
|
||||
float sysTextW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, sysBuf).x;
|
||||
float sysTextX = barX + barW - textPadX - sysTextW;
|
||||
|
||||
@@ -326,7 +326,7 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
|
||||
? it->second.feePercent
|
||||
: kp.feePercent;
|
||||
if (feePct >= 0.0)
|
||||
snprintf(right, sizeof(right), "%s %s%% fee", hrStr.c_str(),
|
||||
snprintf(right, sizeof(right), TR("grpc_hashrate_fee"), hrStr.c_str(),
|
||||
FormatFeePercent(feePct).c_str());
|
||||
else
|
||||
snprintf(right, sizeof(right), "%s", hrStr.c_str());
|
||||
@@ -421,9 +421,10 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
|
||||
// centered ~1000dp band inside the panel's padded content region; the
|
||||
// glass panel itself still fills rightW, leftover -> side margin.
|
||||
const float rightContentW = rightW - pad * 2.0f;
|
||||
const float chartBandW = std::min(rightContentW, 1000.0f * dp);
|
||||
const float chartBandX = rightMin.x + pad
|
||||
+ std::max(0.0f, (rightContentW - chartBandW) * 0.5f);
|
||||
// Fill the panel's content width so the chart extends across the whole panel on a wide window.
|
||||
// (Previously capped at ~1000dp and centered, which left large empty margins either side.)
|
||||
const float chartBandW = rightContentW;
|
||||
const float chartBandX = rightMin.x + pad;
|
||||
|
||||
// Right panel: live log (if toggled + available) else the sparkline.
|
||||
if (showLogView) {
|
||||
|
||||
@@ -263,8 +263,7 @@ static void RenderMiningTabContent(App* app)
|
||||
}
|
||||
if (benchmarkUpdate.inconclusive) {
|
||||
Notifications::instance().warning(
|
||||
"Benchmark inconclusive: no hashrate samples were recorded. "
|
||||
"Check the pool connection and try again.");
|
||||
TR("grpc_benchmark_inconclusive"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -380,7 +380,7 @@ void RenderPeersTab(App* app)
|
||||
if (tlsCount == totalPeers) {
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
ImVec2 txtSize = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf);
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4, valY), Success(), ICON_MD_CHECK);
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4 * dp, valY), Success(), ICON_MD_CHECK);
|
||||
}
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
@@ -695,7 +695,7 @@ void RenderPeersTab(App* app)
|
||||
ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size * dp);
|
||||
ImVec2 pillMax(dirX + dirSz.x + S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + capFont->LegacySize + S.drawElement("tabs.peers", "dir-pill-y-bottom").size * dp);
|
||||
dl->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size * dp);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2), dirFg, dirLabel);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2 * dp), dirFg, dirLabel);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -745,9 +745,9 @@ void RenderPeersTab(App* app)
|
||||
ImU32 tlsBg = WithAlpha(Success(), 25);
|
||||
ImU32 tlsFg = WithAlpha(Success(), 200);
|
||||
ImVec2 tlsMin(badgeX, cy2);
|
||||
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2);
|
||||
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2 * dp);
|
||||
dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size * dp);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4, cy2 + 1), tlsFg, "TLS");
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4 * dp, cy2 + 1 * dp), tlsFg, "TLS");
|
||||
} else {
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2),
|
||||
WithAlpha(Error(), 140), TR("peers_no_tls"));
|
||||
|
||||
@@ -259,11 +259,11 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
snprintf(buf, sizeof(buf), "%s %s (%s) \xe2\x80\x94 %.8f %s%s",
|
||||
tag, lblIt->second.c_str(), trunc.c_str(),
|
||||
addr.balance, DRAGONX_TICKER,
|
||||
isNew ? " [NEW]" : "");
|
||||
isNew ? TR("grpb_new_badge_suffix") : "");
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s%s",
|
||||
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER,
|
||||
isNew ? " [NEW]" : "");
|
||||
isNew ? TR("grpb_new_badge_suffix") : "");
|
||||
}
|
||||
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
@@ -272,9 +272,9 @@ static void RenderAddressDropdown(App* app, float width) {
|
||||
s_cached_qr_data.clear(); // Force QR regeneration
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
material::Tooltip("%s\nBalance: %.8f %s%s",
|
||||
material::Tooltip(TR("grpb_tooltip_address_balance"),
|
||||
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
|
||||
isCurrent ? "\n(selected)" : "");
|
||||
isCurrent ? TR("grpb_selected_suffix") : "");
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -716,17 +716,17 @@ void RenderReceiveTab(App* app)
|
||||
ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
float ss = iconW * 0.5f;
|
||||
float cx = startX + ss;
|
||||
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size;
|
||||
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size * cardDp;
|
||||
float al = ss * S.drawElement("tabs.receive", "swap-icon-arrow-length-ratio").size;
|
||||
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size;
|
||||
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size * cardDp;
|
||||
float ay1 = cy - ag;
|
||||
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size);
|
||||
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
|
||||
dl->AddTriangleFilled(
|
||||
ImVec2(cx + al, ay1),
|
||||
ImVec2(cx + al - hss, ay1 - hss),
|
||||
ImVec2(cx + al - hss, ay1 + hss), iconCol);
|
||||
float ay2 = cy + ag;
|
||||
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size);
|
||||
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
|
||||
dl->AddTriangleFilled(
|
||||
ImVec2(cx - al, ay2),
|
||||
ImVec2(cx - al + hss, ay2 - hss),
|
||||
@@ -744,8 +744,8 @@ void RenderReceiveTab(App* app)
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size;
|
||||
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size;
|
||||
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size;
|
||||
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size * cardDp;
|
||||
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size * cardDp;
|
||||
|
||||
struct Preset { const char* label; double amount; };
|
||||
Preset presets[] = {
|
||||
@@ -873,7 +873,7 @@ void RenderReceiveTab(App* app)
|
||||
RenderQRCode(s_qr_texture, qrSize);
|
||||
} else {
|
||||
ImGui::Dummy(ImVec2(qrSize, qrSize));
|
||||
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size,
|
||||
ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size * cardDp,
|
||||
qrPanelMin.y + totalQrSize * 0.5f);
|
||||
dl->AddText(capFont, capFont->LegacySize, textPos,
|
||||
OnSurfaceDisabled(), TR("qr_unavailable"));
|
||||
|
||||
@@ -364,7 +364,8 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
|
||||
if (suggestions.empty()) return;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor(schema::UI().drawElement("tabs.send", "suggestion-bg-color").color)));
|
||||
float sugH = std::min((float)suggestions.size() * schema::UI().drawElement("tabs.send", "suggestion-row-height").size + schema::UI().drawElement("tabs.send", "suggestion-list-padding").size, schema::UI().drawElement("tabs.send", "suggestion-max-height").size);
|
||||
const float dp = Layout::dpiScale();
|
||||
float sugH = std::min((float)suggestions.size() * schema::UI().drawElement("tabs.send", "suggestion-row-height").size * dp + schema::UI().drawElement("tabs.send", "suggestion-list-padding").size * dp, schema::UI().drawElement("tabs.send", "suggestion-max-height").size * dp);
|
||||
ImGui::BeginChild(childId, ImVec2(width, sugH), true);
|
||||
for (size_t si = 0; si < suggestions.size(); si++) {
|
||||
int sugTrunc = (int)schema::UI().drawElement("tabs.send", "suggestion-trunc-len").size;
|
||||
@@ -387,13 +388,14 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
|
||||
// ============================================================================
|
||||
static void RenderFeeTierSelector(const char* suffix = "") {
|
||||
auto& S = schema::UI();
|
||||
const float dp = Layout::dpiScale();
|
||||
const char* feeLabels[] = { TR("send_fee_low"), TR("send_fee_normal"), TR("send_fee_high") };
|
||||
const double feeValues[] = { FEE_LOW, FEE_NORMAL, FEE_HIGH };
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "fee-rounding").size);
|
||||
for (int fi = 0; fi < 3; fi++) {
|
||||
if (fi > 0) ImGui::SameLine(0, S.drawElement("tabs.send", "fee-tier-gap").size);
|
||||
if (fi > 0) ImGui::SameLine(0, S.drawElement("tabs.send", "fee-tier-gap").size * dp);
|
||||
bool active = (s_fee_tier == fi);
|
||||
if (active) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.send", "fee-tier-active-bg-alpha").size)));
|
||||
@@ -515,7 +517,7 @@ static void RenderAmountBar(ImDrawList* dl, double available, float innerW,
|
||||
// Max button — use caption font to fit bar height
|
||||
ImGui::SameLine(0, gap);
|
||||
char maxId[32];
|
||||
snprintf(maxId, sizeof(maxId), "Max%s", suffix);
|
||||
snprintf(maxId, sizeof(maxId), "%s%s", TR("grpb_max"), suffix);
|
||||
if (TactileButton(maxId, ImVec2(maxBtnW, barH), capFont)) {
|
||||
s_amount = maxAmount;
|
||||
s_send_max = true;
|
||||
@@ -976,6 +978,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
bool is_valid_address, double available,
|
||||
const char* suffix = "") {
|
||||
auto& S = schema::UI();
|
||||
const float dp = Layout::dpiScale();
|
||||
const auto& state = app->getWalletState();
|
||||
double total = s_amount + s_fee;
|
||||
// Block spending from a view-only source (imported viewing key, no spending key) — it would only
|
||||
@@ -1065,12 +1068,12 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
if (ImGui::BeginPopup(confirmClearId)) {
|
||||
ImGui::Text("%s", TR("send_clear_fields"));
|
||||
ImGui::Spacing();
|
||||
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size, 0), S.resolveFont("button"))) {
|
||||
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size * dp, 0), S.resolveFont("button"))) {
|
||||
ClearFormWithUndo();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size, 0), S.resolveFont("button"))) {
|
||||
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size * dp, 0), S.resolveFont("button"))) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
@@ -1086,7 +1089,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-bg-alpha").size)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(WithAlpha(Warning(), (int)S.drawElement("tabs.send", "undo-btn-hover-alpha").size)));
|
||||
char undoId[32];
|
||||
snprintf(undoId, sizeof(undoId), "Undo Clear%s", suffix);
|
||||
snprintf(undoId, sizeof(undoId), "%s%s", TR("grpb_undo_clear"), suffix);
|
||||
if (TactileButton(undoId, ImVec2(width, btnH), S.resolveFont("button"))) {
|
||||
RestoreFormSnapshot();
|
||||
Notifications::instance().info(TR("send_form_restored"));
|
||||
|
||||
@@ -216,7 +216,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
if (!skin.valid) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||||
ImGui::BeginDisabled(true);
|
||||
std::string label = skin.name + " (invalid)";
|
||||
std::string label = skin.name + TR("swin_invalid_suffix");
|
||||
ImGui::Selectable(label.c_str(), false);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleColor();
|
||||
@@ -261,7 +261,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
ImGui::PushFont(material::Type().iconSmall());
|
||||
if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) {
|
||||
skinMgr.refresh();
|
||||
Notifications::instance().info("Theme list refreshed");
|
||||
Notifications::instance().info(TR("swin_theme_list_refreshed"));
|
||||
}
|
||||
ImGui::PopFont();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
@@ -425,14 +425,14 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
|
||||
if (error.empty()) {
|
||||
std::string version = result.value("version", "unknown");
|
||||
std::string msg = "Connection successful!\ndragonxd version: " + version;
|
||||
std::string msg = std::string(TR("swin_connection_successful")) + version;
|
||||
Notifications::instance().success(msg);
|
||||
} else {
|
||||
Notifications::instance().error("Connection failed: " + error);
|
||||
Notifications::instance().error(std::string(TR("swin_connection_failed")) + error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Notifications::instance().error("RPC client not initialized");
|
||||
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,15 +455,15 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
if (error.empty()) {
|
||||
int start = result.value("start_height", 0);
|
||||
int end = result.value("stop_height", 0);
|
||||
std::string msg = "Rescan started from block " + std::to_string(start) +
|
||||
" to " + std::to_string(end);
|
||||
std::string msg = std::string(TR("swin_rescan_started_from_block")) + std::to_string(start) +
|
||||
TR("swin_rescan_to") + std::to_string(end);
|
||||
Notifications::instance().success(msg);
|
||||
} else {
|
||||
Notifications::instance().error("Rescan failed: " + error);
|
||||
Notifications::instance().error(std::string(TR("swin_rescan_failed")) + error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Notifications::instance().error("RPC client not initialized");
|
||||
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
|
||||
}
|
||||
}
|
||||
ImGui::TextDisabled(" %s", TR("settings_rescan_desc"));
|
||||
@@ -503,9 +503,9 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
if (doConfirm) {
|
||||
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
|
||||
if (util::Platform::deleteFile(ztx_file)) {
|
||||
Notifications::instance().success("Z-transaction history cleared");
|
||||
Notifications::instance().success(TR("swin_ztx_history_cleared"));
|
||||
} else {
|
||||
Notifications::instance().info("No history file found");
|
||||
Notifications::instance().info(TR("swin_no_history_file_found"));
|
||||
}
|
||||
s_confirm_clear_ztx = false;
|
||||
}
|
||||
@@ -568,7 +568,7 @@ void RenderSettingsWindow(App* app, bool* p_open)
|
||||
// Save/Cancel buttons
|
||||
if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
|
||||
saveSettingsFromUI(app->settings());
|
||||
Notifications::instance().success("Settings saved");
|
||||
Notifications::instance().success(TR("swin_settings_saved"));
|
||||
*p_open = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
@@ -124,7 +124,7 @@ void TransactionDetailsDialog::render(App* app)
|
||||
ImGui::SetNextItemWidth(txidInput.width); // negative = fill, reserving |width| px for the Copy button (a content-region margin, not raw px — must NOT be dpi-scaled)
|
||||
ImGui::InputText("##TxID", txid_buf, sizeof(txid_buf), ImGuiInputTextFlags_ReadOnly);
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("Copy##TxID", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
|
||||
if (material::StyledButton((std::string(TR("grpb_copy")) + "##TxID").c_str(), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
|
||||
ImGui::SetClipboardText(tx.txid.c_str());
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ void TransactionDetailsDialog::render(App* app)
|
||||
ImGui::InputText("##Address", addr_buf, sizeof(addr_buf), ImGuiInputTextFlags_ReadOnly);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton("Copy##Addr", ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
|
||||
if (material::StyledButton((std::string(TR("grpb_copy")) + "##Addr").c_str(), ImVec2(copyBtn.width, 0), S.resolveFont(copyBtn.font))) {
|
||||
ImGui::SetClipboardText(tx.address.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,10 +263,10 @@ void RenderTransactionsTab(App* app)
|
||||
int idx_map[] = {-1, 1, 0, 2};
|
||||
int idx = idx_map[type_filter];
|
||||
float xOff = idx * (cardW + cardGap);
|
||||
ImVec2 acMin(origin.x + xOff, origin.y + cardH - 3);
|
||||
ImVec2 acMin(origin.x + xOff, origin.y + cardH - 3 * vs);
|
||||
ImVec2 acMax(origin.x + xOff + cardW, origin.y + cardH);
|
||||
ImU32 acCol = (type_filter == 1) ? redCol : (type_filter == 2) ? greenCol : goldCol;
|
||||
dl->AddRectFilled(acMin, acMax, acCol, 2.0f);
|
||||
dl->AddRectFilled(acMin, acMax, acCol, 2.0f * hs);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(availWidth, cardH));
|
||||
|
||||
@@ -100,7 +100,7 @@ public:
|
||||
const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title
|
||||
+ Type().caption()->LegacySize + Layout::spacingSm() // intro + gap
|
||||
+ ctrlRow + Layout::spacingSm(); // sort control row + gap
|
||||
const float padV = 48.0f; // content-child padding (top+bottom) + margin
|
||||
const float padV = 48.0f * dp; // content-child padding (top+bottom) + margin
|
||||
// Size to content, but cap at a viewport fraction: with many wallets on a small / HiDPI
|
||||
// screen the content-sized card could exceed the window (the framework would clip a too-tall
|
||||
// card, footer and all). When capped, the wallet list flexes + scrolls and the controls
|
||||
|
||||
Reference in New Issue
Block a user