// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "mining_stats.h" #include "mining_tab_helpers.h" // FormatHashrate, EstimateHoursToBlock #include "../../app.h" #include "../../config/settings.h" #include "../../util/i18n.h" #include "../../util/pool_registry.h" #include "../schema/ui_schema.h" #include "../material/type.h" #include "../material/draw_helpers.h" #include "../material/colors.h" #include "../layout.h" #include "../../embedded/IconsMaterialDesign.h" #include "imgui.h" #include #include #include #include #include #include namespace dragonx { namespace ui { using namespace material; // Chart/log view toggles (false = chart, true = live log). Moved here with the card they belong to. static bool s_show_pool_log = false; static bool s_show_solo_log = false; // --------------------------------------------------------------------------- // Sparkline chart of a hashrate history, drawn inside [regionMin, regionMax] // (the plot area within a card, already inset by padding). Extracted so both // the solo full-width card and the pool split card share one implementation. // --------------------------------------------------------------------------- static void DrawHashrateSparkline(ImDrawList* dl, ImVec2 regionMin, ImVec2 regionMax, const std::vector& chartHistory, ImFont* capFont, float dp) { if (chartHistory.size() < 2) return; const float chartBot = regionMax.y; double yMin = *std::min_element(chartHistory.begin(), chartHistory.end()); double yMax = *std::max_element(chartHistory.begin(), chartHistory.end()); if (yMax <= yMin) { yMax = yMin + 1.0; } double yRange = yMax - yMin; double yPad2 = yRange * 0.1; yMin -= yPad2; yMax += yPad2; const float plotLeft = regionMin.x; const float plotRight = regionMax.x; const float plotTop = regionMin.y + capFont->LegacySize + 4 * dp; const float plotBottom = chartBot - capFont->LegacySize * 2 - 16 * dp; const float plotW = plotRight - plotLeft; const float plotH = std::max(1.0f, plotBottom - plotTop); size_t n = chartHistory.size(); float stepW = (n > 1) ? plotW / (float)(n - 1) : plotW; std::vector rawPts(n); for (size_t i = 0; i < n; i++) { float x = plotLeft + (float)i * stepW; float y = plotBottom - (float)((chartHistory[i] - yMin) / (yMax - yMin)) * plotH; rawPts[i] = ImVec2(x, y); } // Catmull-Rom spline interpolation for a smooth curve (adaptive subdivision). std::vector points; if (n <= 2) { points = rawPts; } else { points.reserve(n * 4); for (size_t i = 0; i + 1 < n; i++) { ImVec2 p0 = rawPts[i > 0 ? i - 1 : 0]; ImVec2 p1 = rawPts[i]; ImVec2 p2 = rawPts[i + 1]; ImVec2 p3 = rawPts[i + 2 < n ? i + 2 : n - 1]; float dx = p2.x - p1.x, dy = p2.y - p1.y; float dist = sqrtf(dx * dx + dy * dy); int subdivs = std::clamp((int)(dist / 3.0f), 1, 16); for (int s = 0; s < subdivs; s++) { float t = (float)s / (float)subdivs; float t2 = t * t; float t3 = t2 * t; float q0 = -t3 + 2.0f * t2 - t; float q1 = 3.0f * t3 - 5.0f * t2 + 2.0f; float q2 = -3.0f * t3 + 4.0f * t2 + t; float q3 = t3 - t2; float sx = 0.5f * (p0.x * q0 + p1.x * q1 + p2.x * q2 + p3.x * q3); float sy = 0.5f * (p0.y * q0 + p1.y * q1 + p2.y * q2 + p3.y * q3); sy = std::clamp(sy, plotTop, plotBottom); points.push_back(ImVec2(sx, sy)); } } points.push_back(rawPts[n - 1]); } if (points.size() >= 2) { for (size_t i = 0; i < points.size(); i++) dl->PathLineTo(points[i]); dl->PathLineTo(ImVec2(points.back().x, plotBottom)); dl->PathLineTo(ImVec2(points.front().x, plotBottom)); dl->PathFillConcave(WithAlpha(Success(), 25)); } dl->AddPolyline(points.data(), (int)points.size(), WithAlpha(Success(), 200), ImDrawFlags_None, schema::UI().drawElement("tabs.mining", "chart-line-thickness").size); // Y-axis labels std::string yMaxStr = FormatHashrate(yMax); std::string yMinStr = FormatHashrate(yMin); dl->AddText(capFont, capFont->LegacySize, ImVec2(plotLeft + 2 * dp, plotTop - capFont->LegacySize - 2 * dp), OnSurfaceDisabled(), yMaxStr.c_str()); dl->AddText(capFont, capFont->LegacySize, ImVec2(plotLeft + 2 * dp, plotBottom + 4 * dp), OnSurfaceDisabled(), yMinStr.c_str()); // X-axis labels dl->AddText(capFont, capFont->LegacySize, ImVec2(plotLeft, chartBot - capFont->LegacySize - 2 * dp), OnSurfaceDisabled(), chartHistory.size() >= 300 ? TR("mining_chart_5m_ago") : chartHistory.size() >= 60 ? TR("mining_chart_1m_ago") : TR("mining_chart_start")); std::string nowLbl = TR("mining_chart_now"); ImVec2 nowSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, nowLbl.c_str()); dl->AddText(capFont, capFont->LegacySize, ImVec2(plotRight - nowSz.x, chartBot - capFont->LegacySize - 2 * dp), OnSurfaceDisabled(), nowLbl.c_str()); } // --------------------------------------------------------------------------- // Read-only, selectable/copyable log view filling [regionMin, size]. // --------------------------------------------------------------------------- static void DrawMiningLogView(ImVec2 regionMin, float regionW, float regionH, const std::vector& logLines, const char* inputId) { static std::string s_log_buf; s_log_buf.clear(); for (const auto& line : logLines) if (!line.empty()) { s_log_buf += line; s_log_buf += '\n'; } ImGui::SetCursorScreenPos(regionMin); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurface())); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 0)); ImGui::PushFont(Type().body2()); ImGui::InputTextMultiline(inputId, const_cast(s_log_buf.c_str()), s_log_buf.size() + 1, ImVec2(regionW, regionH), ImGuiInputTextFlags_ReadOnly); ImGui::PopFont(); ImGui::PopStyleColor(2); } // --------------------------------------------------------------------------- // The left pool card (pool mode only): Manual/Auto toggle, a metric stack, and // a fixed-height scrolling pool list. Pool rows are clickable to select in // Manual mode (routed through the parent's dirty flag → save + restart) and // show a "mining here" dot in Auto mode. The list scrolls, so any number of // pools keeps the card — and everything below it — at a constant height. // --------------------------------------------------------------------------- static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont, float dp, float gap, float pad, ImVec2 cardMin, ImVec2 cardMax, char (&s_pool_url)[256], bool& s_pool_settings_dirty) { const bool autoMode = app->settings()->getPoolSelectMode() == config::Settings::PoolSelectMode::AutoBalance; const float x = cardMin.x + pad; const float w = (cardMax.x - cardMin.x) - pad * 2; float y = cardMin.y + pad * 0.75f; const float toggleH = sub1->LegacySize + 10 * dp; const float rowH = sub1->LegacySize + 7 * dp; // --- Manual | Auto-balance toggle --- { ImFont* segFont = Type().caption(); const char* segA = TR("mining_select_manual"); const char* segB = TR("mining_select_auto"); const float rnd = 4 * dp; const float half = w * 0.5f; const ImVec2 aMin(x, y), aMax(x + half, y + toggleH); const ImVec2 bMin(x + half, y), bMax(x + w, y + toggleH); dl->AddRectFilled(aMin, bMax, WithAlpha(OnSurface(), 15), rnd); dl->AddRect(aMin, bMax, WithAlpha(OnSurface(), 40), rnd); auto seg = [&](const ImVec2& mn, const ImVec2& mx, const char* label, bool selected) { if (selected) dl->AddRectFilled(mn, mx, WithAlpha(Primary(), 180), rnd); else if (material::IsRectHovered(mn, mx)) dl->AddRectFilled(mn, mx, WithAlpha(OnSurface(), 20), rnd); ImVec2 sz = segFont->CalcTextSizeA(segFont->LegacySize, FLT_MAX, 0, label); dl->AddText(segFont, segFont->LegacySize, ImVec2(mn.x + (mx.x - mn.x - sz.x) * 0.5f, mn.y + (mx.y - mn.y - sz.y) * 0.5f), selected ? IM_COL32(255, 255, 255, 230) : OnSurfaceMedium(), label); }; seg(aMin, aMax, segA, !autoMode); seg(bMin, bMax, segB, autoMode); ImGui::SetCursorScreenPos(aMin); ImGui::InvisibleButton("##CardSelManual", ImVec2(half, toggleH)); if (ImGui::IsItemClicked() && autoMode) { app->settings()->setPoolSelectMode(config::Settings::PoolSelectMode::Manual); app->settings()->save(); } if (material::IsRectHovered(aMin, aMax)) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); material::Tooltip("%s", TR("mining_manual_desc")); } ImGui::SetCursorScreenPos(bMin); ImGui::InvisibleButton("##CardSelAuto", ImVec2(w - half, toggleH)); if (ImGui::IsItemClicked() && !autoMode) { app->settings()->setPoolSelectMode(config::Settings::PoolSelectMode::AutoBalance); app->settings()->save(); app->requestPoolBalanceRefresh(); } if (material::IsRectHovered(bMin, bMax)) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); material::Tooltip("%s", TR("mining_auto_balance_desc")); } } y += toggleH + gap; // --- Metric stack (label left, value right) --- auto metricRow = [&](const char* label, const std::string& val, ImU32 valCol) { dl->AddText(ovFont, ovFont->LegacySize, ImVec2(x, y + (rowH - ovFont->LegacySize) * 0.5f), OnSurfaceMedium(), label); ImVec2 vSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, val.c_str()); dl->AddText(sub1, sub1->LegacySize, ImVec2(x + w - vSz.x, y + (rowH - sub1->LegacySize) * 0.5f), valCol, val.c_str()); y += rowH; }; const auto& pm = state.pool_mining; metricRow(TR("mining_local_hashrate"), FormatHashrate(pm.hashrate_10s), pm.xmrig_running ? Success() : OnSurfaceDisabled()); metricRow(TR("mining_pool_hashrate"), FormatHashrate(pm.pool_hashrate), pm.pool_hashrate > 0 ? OnSurface() : OnSurfaceDisabled()); { char sharesBuf[64]; snprintf(sharesBuf, sizeof(sharesBuf), "%lld / %lld", (long long)pm.accepted, (long long)pm.rejected); metricRow(TR("mining_shares"), sharesBuf, OnSurface()); } { char uptBuf[64]; int64_t up = pm.uptime_sec; if (up <= 0) snprintf(uptBuf, sizeof(uptBuf), "N/A"); else if (up < 3600) snprintf(uptBuf, sizeof(uptBuf), "%lldm %llds", (long long)(up / 60), (long long)(up % 60)); else snprintf(uptBuf, sizeof(uptBuf), "%lldh %lldm", (long long)(up / 3600), (long long)((up % 3600) / 60)); metricRow(TR("mining_uptime"), uptBuf, OnSurface()); } y += gap * 0.5f; // --- POOLS (N) header + Refresh --- { char hdr[48]; snprintf(hdr, sizeof(hdr), "%s (%d)", TR("mining_pools_header"), (int)util::knownPools().size()); dl->AddText(ovFont, ovFont->LegacySize, ImVec2(x, y), OnSurfaceMedium(), hdr); float btnS = ovFont->LegacySize + 6 * dp; ImVec2 rbMin(x + w - btnS, y - 2 * dp); material::IconButtonStyle st; st.color = OnSurfaceMedium(); st.hoverBg = StateHover(); st.bgRounding = 3 * dp; st.tooltip = TR("mining_refresh"); ImGui::SetCursorScreenPos(rbMin); if (material::IconButton("##PoolRefresh", ICON_MD_REFRESH, Type().iconSmall(), ImVec2(btnS, btnS), st)) app->requestPoolBalanceRefresh(); } y += ovFont->LegacySize + 4 * dp; // --- Scrollable pool list (fixed height → constant card height for any N) --- const float listH = std::max(rowH, cardMax.y - y - pad * 0.75f); ImGui::SetCursorScreenPos(ImVec2(x, y)); ImGui::BeginChild("##PoolList", ImVec2(w, listH), false, ImGuiWindowFlags_NoBackground); { ImDrawList* cdl = ImGui::GetWindowDrawList(); const auto snap = app->poolStatsSnapshot(); const util::KnownPool* current = util::findKnownPoolByUrl(app->settings()->getPoolUrl()); const float childW = ImGui::GetContentRegionAvail().x; const float listRowH = capFont->LegacySize + 10 * dp; for (const auto& kp : util::knownPools()) { ImGui::PushID(kp.id.c_str()); const bool isCurrent = current && current->id == kp.id; const auto it = snap.byId.find(kp.id); const bool haveHr = (it != snap.byId.end() && it->second.ok); ImVec2 rMin = ImGui::GetCursorScreenPos(); ImVec2 rMax(rMin.x + childW, rMin.y + listRowH); bool hov = false, clk = false; if (!autoMode) { ImGui::InvisibleButton("##prow", ImVec2(childW, listRowH)); hov = ImGui::IsItemHovered(); clk = ImGui::IsItemClicked(); } else { ImGui::Dummy(ImVec2(childW, listRowH)); } if (isCurrent) cdl->AddRectFilled(rMin, rMax, WithAlpha(Primary(), 30), 3 * dp); else if (hov) cdl->AddRectFilled(rMin, rMax, StateHover(), 3 * dp); const float cy = rMin.y + listRowH * 0.5f; cdl->AddCircleFilled(ImVec2(rMin.x + 6 * dp, cy), 3.0f * dp, isCurrent ? Success() : OnSurfaceDisabled()); const float textY = rMin.y + (listRowH - capFont->LegacySize) * 0.5f; // Show the short host label (the narrow card can't fit host:port); the // full stratum URL is in the hover tooltip. cdl->AddText(capFont, capFont->LegacySize, ImVec2(rMin.x + 16 * dp, textY), isCurrent ? Primary() : OnSurface(), kp.label.c_str()); char right[64]; std::string hrStr = haveHr ? FormatHashrate(it->second.hashrateHs) : std::string("—"); snprintf(right, sizeof(right), "%s %.0f%%", hrStr.c_str(), kp.feePercent); ImVec2 rSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, right); cdl->AddText(capFont, capFont->LegacySize, ImVec2(rMax.x - rSz.x - 6 * dp, textY), OnSurfaceMedium(), right); if (!autoMode && hov) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); material::Tooltip("%s", kp.stratum.c_str()); } if (clk) { strncpy(s_pool_url, kp.stratum.c_str(), sizeof(s_pool_url) - 1); s_pool_url[sizeof(s_pool_url) - 1] = '\0'; s_pool_settings_dirty = true; } ImGui::PopID(); } } ImGui::EndChild(); } void RenderMiningStats(const WalletState& state, const MiningInfo& mining, ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont, float dp, float vs, float gap, float pad, float availWidth, const GlassPanelSpec& glassSpec, float chartBudgetH, bool poolMode, App* app, char (&s_pool_url)[256], bool& s_pool_settings_dirty) { (void)vs; const bool s_pool_mode = poolMode; ImU32 greenCol = Success(); bool showLogView = s_pool_mode ? (s_show_pool_log && !state.pool_mining.log_lines.empty()) : (s_show_solo_log && !mining.log_lines.empty()); bool hasLogContent = s_pool_mode ? !state.pool_mining.log_lines.empty() : !mining.log_lines.empty(); const std::vector& chartHistory = s_pool_mode ? state.pool_mining.hashrate_history : mining.hashrate_history; bool hasChartContent = chartHistory.size() >= 2; float statRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + Layout::spacingSm(); bool& showLogFlag = s_pool_mode ? s_show_pool_log : s_show_solo_log; // ================================================================ // POOL MODE — split card: [ left pool card ][ chart / log ] // ================================================================ if (s_pool_mode) { // Left card height driven by its content so the pool list always fits; // the whole row is at least as tall as the old stats+chart card. const float toggleH = sub1->LegacySize + 10 * dp; const float mRowH = sub1->LegacySize + 7 * dp; const float listMinH = (capFont->LegacySize + 10 * dp) * 2.6f; // ~2–3 rows before scroll float leftContentH = pad * 0.75f + toggleH + gap + mRowH * 4.0f + gap * 0.5f + (ovFont->LegacySize + 4 * dp) + listMinH + pad * 0.75f; float cardH = std::max(leftContentH, statRowH + chartBudgetH + pad); ImVec2 cardMin = ImGui::GetCursorScreenPos(); float leftW = std::clamp(availWidth * 0.25f, 180.0f * dp, availWidth * 0.45f); float colGap = gap; float rightW = availWidth - leftW - colGap; ImVec2 leftMin = cardMin; ImVec2 leftMax(cardMin.x + leftW, cardMin.y + cardH); ImVec2 rightMin(leftMax.x + colGap, cardMin.y); ImVec2 rightMax(cardMin.x + availWidth, cardMin.y + cardH); DrawGlassPanel(dl, leftMin, leftMax, glassSpec); DrawGlassPanel(dl, rightMin, rightMax, glassSpec); RenderLeftPoolCard(app, state, dl, capFont, sub1, ovFont, dp, gap, pad, leftMin, leftMax, s_pool_url, s_pool_settings_dirty); // Right panel: live log (if toggled + available) else the sparkline. if (showLogView) { float logPad = pad * 0.5f; DrawMiningLogView(ImVec2(rightMin.x + logPad, rightMin.y + logPad), rightW - logPad * 2, cardH - logPad * 2, state.pool_mining.log_lines, "##PoolLogText"); } else if (hasChartContent) { DrawHashrateSparkline(dl, ImVec2(rightMin.x + pad, rightMin.y + statRowH * 0.5f), ImVec2(rightMax.x - pad, rightMax.y), chartHistory, capFont, dp); } else { const char* hint = TR("mining_chart_start"); ImVec2 hs = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, hint); dl->AddText(capFont, capFont->LegacySize, ImVec2((rightMin.x + rightMax.x - hs.x) * 0.5f, (rightMin.y + rightMax.y - hs.y) * 0.5f), OnSurfaceDisabled(), hint); } // Chart/log toggle button, top-right of the RIGHT panel. if (hasLogContent || hasChartContent) { ImFont* iconFont = Type().iconSmall(); const char* toggleIcon = showLogFlag ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE; const char* toggleTip = showLogFlag ? TR("mining_show_chart") : TR("mining_show_log"); ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, toggleIcon); float btnSize = iconSz.y + 8 * dp; ImVec2 btnMin(rightMax.x - pad - btnSize, rightMin.y + pad * 0.5f); ImVec2 btnMax(btnMin.x + btnSize, btnMin.y + btnSize); ImVec2 btnCenter((btnMin.x + btnMax.x) * 0.5f, (btnMin.y + btnMax.y) * 0.5f); bool hov = IsRectHovered(btnMin, btnMax); if (hov) { dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover()); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); material::Tooltip("%s", toggleTip); } dl->AddText(iconFont, iconFont->LegacySize, ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f), OnSurfaceMedium(), toggleIcon); if (hov && ImGui::IsMouseClicked(0)) showLogFlag = !showLogFlag; } ImGui::SetCursorScreenPos(cardMin); ImGui::Dummy(ImVec2(availWidth, cardH)); ImGui::Dummy(ImVec2(0, gap)); return; } // ================================================================ // SOLO MODE — full-width card: stat columns on top, chart/log below // ================================================================ float totalCardH = statRowH + chartBudgetH + pad; ImVec2 cardMin = ImGui::GetCursorScreenPos(); ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + totalCardH); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); if (showLogView) { float logPad = pad * 0.5f; DrawMiningLogView(ImVec2(cardMin.x + logPad, cardMin.y + logPad), availWidth - logPad * 2, totalCardH - logPad * 2, mining.log_lines, "##SoloLogText"); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); } else { double est_hours = EstimateHoursToBlock(mining.localHashrate, mining.networkHashrate, mining.difficulty); const char* col1Label = TR("mining_local_hashrate"); std::string col1Str = FormatHashrate(mining.localHashrate); ImU32 col1Col = mining.generate ? greenCol : OnSurfaceDisabled(); const char* col2Label = TR("mining_network"); std::string col2Str = FormatHashrate(mining.networkHashrate); const char* col3Label = TR("mining_est_block"); std::string col3Str = FormatEstTime(est_hours); int numStats = 3; float statColW = (availWidth - pad * 2) / (float)numStats; float sy = cardMin.y + pad * 0.5f; struct StatEntry { const char* label; std::string value; ImU32 col; }; StatEntry stats[] = { { col1Label, col1Str, col1Col }, { col2Label, col2Str, OnSurface() }, { col3Label, col3Str, OnSurface() }, }; for (int si = 0; si < numStats; si++) { float sx = cardMin.x + pad + si * statColW; float centerX = sx + statColW * 0.5f; ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, stats[si].label); dl->AddText(ovFont, ovFont->LegacySize, ImVec2(centerX - lblSz.x * 0.5f, sy), OnSurfaceMedium(), stats[si].label); ImVec2 valSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, stats[si].value.c_str()); float valY = sy + ovFont->LegacySize + Layout::spacingXs(); dl->AddText(sub1, sub1->LegacySize, ImVec2(centerX - valSz.x * 0.5f, valY), stats[si].col, stats[si].value.c_str()); if (si == 0 && chartHistory.size() >= 6) { size_t hn = chartHistory.size(); double recent = (chartHistory[hn-1] + chartHistory[hn-2] + chartHistory[hn-3]) / 3.0; double older = (chartHistory[hn-4] + chartHistory[hn-5] + chartHistory[hn-6]) / 3.0; ImFont* iconFont = Type().iconSmall(); float arrowX = centerX + valSz.x * 0.5f + Layout::spacingSm(); if (recent > older * 1.02) { const char* icon = ICON_MD_TRENDING_UP; ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(arrowX, valY + sub1->LegacySize * 0.5f - iSz.y * 0.5f), WithAlpha(Success(), 220), icon); } else if (recent < older * 0.98) { const char* icon = ICON_MD_TRENDING_DOWN; ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(arrowX, valY + sub1->LegacySize * 0.5f - iSz.y * 0.5f), WithAlpha(Error(), 220), icon); } } } if (hasChartContent) { DrawHashrateSparkline(dl, ImVec2(cardMin.x + pad, cardMin.y + statRowH), ImVec2(cardMax.x - pad, cardMax.y), chartHistory, capFont, dp); } ImGui::Dummy(ImVec2(availWidth, totalCardH)); } if (hasLogContent || hasChartContent) { ImFont* iconFont = Type().iconSmall(); const char* toggleIcon = showLogFlag ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE; const char* toggleTip = showLogFlag ? TR("mining_show_chart") : TR("mining_show_log"); ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, toggleIcon); float btnSize = iconSz.y + 8 * dp; float btnX = cardMax.x - pad - btnSize; float btnY = cardMin.y + pad * 0.5f; ImVec2 btnMin(btnX, btnY); ImVec2 btnMax(btnX + btnSize, btnY + btnSize); ImVec2 btnCenter((btnMin.x + btnMax.x) * 0.5f, (btnMin.y + btnMax.y) * 0.5f); bool hov = IsRectHovered(btnMin, btnMax); if (hov) { dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover()); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); material::Tooltip("%s", toggleTip); } dl->AddText(iconFont, iconFont->LegacySize, ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f), OnSurfaceMedium(), toggleIcon); if (hov && ImGui::IsMouseClicked(0)) { showLogFlag = !showLogFlag; } } ImGui::Dummy(ImVec2(0, gap)); } } // namespace ui } // namespace dragonx