// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "mining_controls.h" #include "mining_tab_helpers.h" #include "mining_benchmark.h" #include "xmrig_download_dialog.h" #include "../../app.h" #include "../../config/version.h" #include "../../config/settings.h" #include "../../util/i18n.h" #include "../../util/platform.h" #include "../../util/address_validation.h" // isValidRecipientAddress — payout validation (M-01) #include "../schema/ui_schema.h" #include "../material/type.h" #include "../material/draw_helpers.h" #include "../material/colors.h" #include "../layout.h" #include "../notifications.h" #include "../../embedded/IconsMaterialDesign.h" #include "imgui.h" #include "imgui_internal.h" #include #include #include #include #include #include // std::atoi (xmrig version compare) namespace dragonx { namespace ui { using namespace material; void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& mining, ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont, float dp, float hs, float vs, float gap, float pad, float availWidth, const GlassPanelSpec& glassSpec, float controlsBudgetH, int max_threads, bool isMiningActive, bool poolMode, const char* poolWorker, const std::string& xmrigLatestTag, ThreadBenchmark& benchmark, int& selectedThreads, bool& dragActive, int& dragAnchorThread) { // Aliases so the moved body keeps its original identifiers verbatim. Mutated state is a // reference to the real static; read-only state is a const copy/ref. const bool s_pool_mode = poolMode; const char* const s_pool_worker = poolWorker; const std::string& s_xmrig_latest_tag = xmrigLatestTag; ThreadBenchmark& s_benchmark = benchmark; int& s_selected_threads = selectedThreads; bool& s_drag_active = dragActive; int& s_drag_anchor_thread = dragAnchorThread; char buf[128]; float miningBtnGap = gap; float miningBtnMaxW = availWidth * schema::UI().drawElement("tabs.mining", "btn-max-width-ratio").size; // Thread-count tiles at an adaptive step (multiples of 1/2/4/8, chosen by core count) plus 1 and the // max, so the tile row stays bounded (~<=24 tiles) instead of one tile per thread — a 192-thread // EPYC would otherwise overflow the card. Exact in-between counts are settable via the input box. int tileStep = 1; if (max_threads > 96) tileStep = 8; else if (max_threads > 48) tileStep = 4; else if (max_threads > 24) tileStep = 2; std::vector threadOptions; threadOptions.push_back(1); for (int v = tileStep; v < max_threads; v += tileStep) if (v > 1) threadOptions.push_back(v); if (max_threads > 1 && threadOptions.back() != max_threads) threadOptions.push_back(max_threads); const int nOpts = (int)threadOptions.size(); // Custom drawlist hit-tests (the thread tiles + the Mine button) bypass ImGui's popup input capture, // so a click on an open dropdown (saved pools / payout addresses) would ALSO fire them. Gate on this // so clicking the dropdown's X (or a row) doesn't bleed through to the tiles/Mine button. const bool anyPopupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel); // --- Compute thread grid layout based on controls card width --- // Estimate controlsW first to compute cols correctly // The Mine button is square (= card height, which scales with DPI), so the width we // reserve for it here must scale too — a RAW clamp under-reserves at >100% scaling, which // over-estimates the grid width and lets the thread cells overflow the card (e.g. at 150%). float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size * dp, miningBtnMaxW) - miningBtnGap; float innerW = estControlsW - pad * 2; float cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size * dp, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f) * dp); float cellGap = std::max(schema::UI().drawElement("tabs.mining", "cell-gap-min").size, cellSz * schema::UI().drawElement("tabs.mining", "cell-gap-ratio").size); int cols = std::max(1, std::min(nOpts, (int)(innerW / (cellSz + cellGap)))); int rows = (nOpts + cols - 1) / cols; float gridW = cols * cellSz + (cols - 1) * cellGap; float gridH = rows * cellSz + (rows - 1) * cellGap; // Card sections: header(label+info+RAM inline) | grid float headerH = capFont->LegacySize + Layout::spacingXs(); float secGap = Layout::spacingLg(); // Card height from actual content, capped by proportional budget float cardH = pad + headerH + secGap + gridH + pad; cardH = std::clamp(cardH, schema::UI().drawElement("tabs.mining", "control-card-min-height").size, controlsBudgetH); // Mining button — sized to match card height (square) float miningBtnSz = cardH; if (miningBtnSz + miningBtnGap > miningBtnMaxW) miningBtnSz = miningBtnMaxW; float controlsW = availWidth - miningBtnSz - miningBtnGap; ImVec2 cardMin = ImGui::GetCursorScreenPos(); ImVec2 cardMax(cardMin.x + controlsW, cardMin.y + cardH); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); float curY = cardMin.y + pad; // --- Header row: "THREADS 4 / 16" + RAM est + active indicator --- { ImVec2 labelPos(cardMin.x + pad, curY); dl->AddText(ovFont, ovFont->LegacySize, labelPos, OnSurfaceMedium(), TR("mining_threads")); float labelW = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, TR("mining_threads")).x; snprintf(buf, sizeof(buf), " %d / %d", s_selected_threads, max_threads); ImVec2 countPos(labelPos.x + labelW, curY); dl->AddText(sub1, sub1->LegacySize, countPos, OnSurface(), buf); // RAM estimate inline (after thread count) // Model matches DragonX RandomX: shared ~2080MB dataset + ~256MB cache (allocated once), // plus ~2MB scratchpad per mining thread VM. { float countW = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf).x; double ram_estimate_mb = schema::UI().drawElement("business", "ram-base-mb").size + schema::UI().drawElement("business", "ram-dataset-mb").size + schema::UI().drawElement("business", "ram-cache-mb").size + s_selected_threads * schema::UI().drawElement("business", "ram-per-thread-mb").size; double ram_estimate_gb = ram_estimate_mb / 1024.0; snprintf(buf, sizeof(buf), " RAM ~%.1fGB", ram_estimate_gb); dl->AddText(capFont, capFont->LegacySize, ImVec2(countPos.x + countW, curY + (sub1->LegacySize - capFont->LegacySize) * 0.5f), OnSurfaceDisabled(), buf); } // Centered, top-aligned thread-count stepper: [-] centered editable number [+]. Lets a // high-core CPU pick an EXACT count that isn't one of the tiles. The input commits on Enter // (not per keystroke) so it doesn't restart the miner while the user is still typing. { auto applyThreads = [&](int tc) { tc = std::clamp(tc, 1, max_threads); if (tc == s_selected_threads) return; if (s_benchmark.active()) return; // the benchmark owns the thread count while it runs (as the grid) s_selected_threads = tc; // shown at once; the App applies it ~0.5 s after the last change app->selectMiningThreads(tc); }; ImFont* stepFont = Type().iconSmall(); // 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; const float totalW = sideW + g + fieldW + g + sideW; float sx = cardMin.x + (controlsW - totalW) * 0.5f; const float sy = curY; ImVec2 savedCur = ImGui::GetCursorScreenPos(); // -/+ buttons drawn in the same style as the thread tiles (rounded rect + border + centered // glyph, same fill/border/rounding). Hit-tested with a real InvisibleButton so they stay // popup-safe. const float stepRound = schema::UI().drawElement("tabs.mining", "cell-rounding").size; // atBound: the stepper can't move further (already at 1 for "-" or max for "+"). When // at the bound the control is a no-op, so render it disabled: dim glyph/border, no hand // cursor, no tooltip — and swallow the click so it reads as inert. auto rectStepBtn = [&](const char* id, const char* glyph, float x, const char* tip, bool atBound) -> bool { ImGui::SetCursorScreenPos(ImVec2(x, sy)); ImGui::InvisibleButton(id, ImVec2(sideW, fieldH)); const bool hov = ImGui::IsItemHovered() && !atBound; const bool clk = ImGui::IsItemClicked() && !atBound; const ImVec2 mn(x, sy), mx(x + sideW, sy + fieldH); dl->AddRectFilled(mn, mx, hov ? WithAlpha(OnSurface(), 25) : WithAlpha(OnSurface(), 8), stepRound); dl->AddRect(mn, mx, WithAlpha(OnSurface(), atBound ? 15 : (hov ? 80 : 35)), stepRound); const ImVec2 gsz = stepFont->CalcTextSizeA(stepFont->LegacySize, FLT_MAX, 0, glyph); dl->AddText(stepFont, stepFont->LegacySize, ImVec2(x + (sideW - gsz.x) * 0.5f, sy + (fieldH - gsz.y) * 0.5f), WithAlpha(OnSurface(), atBound ? 30 : (hov ? 160 : 80)), glyph); if (hov) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (tip && tip[0]) material::Tooltip("%s", tip); } return clk; }; // "-" button (disabled at 1 thread) if (rectStepBtn("##ThreadMinus", ICON_MD_REMOVE, sx, TR("mining_threads_minus_tooltip"), s_selected_threads <= 1 || s_benchmark.active())) applyThreads(s_selected_threads - 1); sx += sideW + g; // Centered editable number: symmetric FramePadding pushes the digits to the field centre. char tb[8]; snprintf(tb, sizeof(tb), "%d", s_selected_threads); float basePad = ImGui::GetStyle().FramePadding.x; float txtW = ImGui::CalcTextSize(tb).x; float centerPad = std::max(basePad, (fieldW - txtW) * 0.5f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(centerPad, ImGui::GetStyle().FramePadding.y)); ImGui::SetCursorScreenPos(ImVec2(sx, sy)); ImGui::SetNextItemWidth(fieldW); int tc = s_selected_threads; const bool benchLocked = s_benchmark.active(); // read-only while the benchmark runs (as -/+) ImGui::BeginDisabled(benchLocked); if (ImGui::InputInt("##ThreadCountInput", &tc, 0, 0, ImGuiInputTextFlags_EnterReturnsTrue)) applyThreads(tc); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("mining_threads_input_tooltip")); ImGui::EndDisabled(); ImGui::PopStyleVar(); sx += fieldW + g; // "+" button (disabled at max_threads) if (rectStepBtn("##ThreadPlus", ICON_MD_ADD, sx, TR("mining_threads_plus_tooltip"), s_selected_threads >= max_threads || s_benchmark.active())) applyThreads(s_selected_threads + 1); ImGui::SetCursorScreenPos(savedCur); } // Idle mining toggle (top-right corner of card) float idleRightEdge = cardMax.x - pad; { bool idleOn = app->settings()->getMineWhenIdle(); bool threadScaling = app->settings()->getIdleThreadScaling(); ImFont* icoFont = Type().iconSmall(); const char* idleIcon = ICON_MD_SCHEDULE; float icoH = icoFont->LegacySize; float btnSz = icoH + 8.0f * dp; float btnX = idleRightEdge - btnSz; float btnY = curY + (headerH - btnSz) * 0.5f; // Idle toggle: active pill + state-colored glyph (cursor saved/restored at block end). material::IconButtonStyle st; st.color = idleOn ? Primary() : OnSurfaceDisabled(); st.restBg = idleOn ? WithAlpha(Primary(), 60) : 0; st.tooltip = idleOn ? TR("mining_idle_on_tooltip") : TR("mining_idle_off_tooltip"); ImVec2 savedCur = ImGui::GetCursorScreenPos(); ImGui::SetCursorScreenPos(ImVec2(btnX, btnY)); if (material::IconButton("##IdleMining", idleIcon, icoFont, ImVec2(btnSz, btnSz), st)) { app->settings()->setMineWhenIdle(!idleOn); app->settings()->save(); } idleRightEdge = btnX - 4.0f * dp; // Thread scaling mode toggle (to the left of idle icon, shown when idle is on) if (idleOn) { const char* scaleIcon = threadScaling ? ICON_MD_TUNE : ICON_MD_POWER_SETTINGS_NEW; float sBtnX = idleRightEdge - btnSz; float sBtnY = btnY; material::IconButtonStyle st; st.color = threadScaling ? Primary() : OnSurfaceMedium(); st.restBg = threadScaling ? WithAlpha(Primary(), 40) : 0; st.tooltip = threadScaling ? TR("mining_idle_scale_on_tooltip") : TR("mining_idle_scale_off_tooltip"); ImGui::SetCursorScreenPos(ImVec2(sBtnX, sBtnY)); if (material::IconButton("##IdleScaleMode", scaleIcon, icoFont, ImVec2(btnSz, btnSz), st)) { app->settings()->setIdleThreadScaling(!threadScaling); app->settings()->save(); } idleRightEdge = sBtnX - 4.0f * dp; } // GPU-aware idle toggle (to the left, when idle is on) // When ON (default): GPU utilization >= 10% counts as "not idle" // When OFF: unrestricted mode, only keyboard/mouse input matters if (idleOn) { bool gpuAware = app->settings()->getIdleGpuAware(); float gBtnX = idleRightEdge - btnSz; float gBtnY = btnY; material::IconButtonStyle st; st.color = gpuAware ? Primary() : OnSurfaceDisabled(); st.restBg = gpuAware ? WithAlpha(Primary(), 40) : 0; st.tooltip = gpuAware ? TR("mining_idle_gpu_on_tooltip") : TR("mining_idle_gpu_off_tooltip"); ImGui::SetCursorScreenPos(ImVec2(gBtnX, gBtnY)); if (material::IconButton("##IdleGpuAware", ICON_MD_MONITOR, icoFont, ImVec2(btnSz, btnSz), st)) { app->settings()->setIdleGpuAware(!gpuAware); app->settings()->save(); } idleRightEdge = gBtnX - 4.0f * dp; } // Idle delay combo renderer (shared by the non-scaling and thread-scaling // branches below). Draws right-aligned at idleRightEdge and advances it. auto renderIdleDelayCombo = [&](const char* comboId) { struct DelayOption { int seconds; const char* label; }; static const DelayOption delays[] = { {30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"} }; int curDelay = app->settings()->getMineIdleDelay(); const char* previewLabel = "2m"; for (const auto& d : delays) { if (d.seconds == curDelay) { previewLabel = d.label; break; } } float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f) * dp; float comboX = idleRightEdge - comboW; float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(comboX, comboY)); ImGui::SetNextItemWidth(comboW); if (ImGui::BeginCombo(comboId, previewLabel, ImGuiComboFlags_NoArrowButton)) { for (const auto& d : delays) { bool selected = (d.seconds == curDelay); if (ImGui::Selectable(d.label, selected)) { app->settings()->setMineIdleDelay(d.seconds); app->settings()->save(); } if (selected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_idle_delay")); idleRightEdge = comboX - 4.0f * dp; }; // Idle delay combo (to the left, when idle is enabled and NOT in thread scaling mode) if (idleOn && !threadScaling) { renderIdleDelayCombo("##IdleDelay"); } // Thread scaling controls: idle delay + active threads / idle threads combos if (idleOn && threadScaling) { int hwThreads = std::max(1, (int)std::thread::hardware_concurrency()); // Idle delay combo renderIdleDelayCombo("##IdleDelayScale"); // Idle threads combo (threads when system is idle) { int curVal = app->settings()->getIdleThreadsIdle(); if (curVal <= 0) curVal = hwThreads; char previewBuf[16]; snprintf(previewBuf, sizeof(previewBuf), "%d", curVal); float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f) * dp; float comboX = idleRightEdge - comboW; float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(comboX, comboY)); ImGui::SetNextItemWidth(comboW); if (ImGui::BeginCombo("##IdleThreadsIdle", previewBuf, ImGuiComboFlags_NoArrowButton)) { for (int t = 1; t <= hwThreads; t++) { char lbl[16]; snprintf(lbl, sizeof(lbl), "%d", t); bool selected = (t == curVal); if (ImGui::Selectable(lbl, selected)) { app->settings()->setIdleThreadsIdle(t); app->settings()->save(); } if (selected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("mining_idle_threads_idle_tooltip")); idleRightEdge = comboX - 4.0f * dp; } // Separator arrow icon { const char* arrowIcon = ICON_MD_ARROW_BACK; ImVec2 arrSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, arrowIcon); float arrX = idleRightEdge - arrSz.x; float arrY = curY + (headerH - arrSz.y) * 0.5f; dl->AddText(icoFont, icoFont->LegacySize, ImVec2(arrX, arrY), OnSurfaceDisabled(), arrowIcon); idleRightEdge = arrX - 4.0f * dp; } // Active threads combo (threads when user is active) { int curVal = app->settings()->getIdleThreadsActive(); if (curVal <= 0) curVal = std::max(1, hwThreads / 2); char previewBuf[16]; snprintf(previewBuf, sizeof(previewBuf), "%d", curVal); float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f) * dp; float comboX = idleRightEdge - comboW; float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(comboX, comboY)); ImGui::SetNextItemWidth(comboW); if (ImGui::BeginCombo("##IdleThreadsActive", previewBuf, ImGuiComboFlags_NoArrowButton)) { for (int t = 1; t <= hwThreads; t++) { char lbl[16]; snprintf(lbl, sizeof(lbl), "%d", t); bool selected = (t == curVal); if (ImGui::Selectable(lbl, selected)) { app->settings()->setIdleThreadsActive(t); app->settings()->save(); } if (selected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("mining_idle_threads_active_tooltip")); idleRightEdge = comboX - 4.0f * dp; } } ImGui::SetCursorScreenPos(savedCur); } // --- Thread Benchmark button / progress (left of idle toggle) --- { ImVec2 benchSavedCur = ImGui::GetCursorScreenPos(); bool benchRunning = s_benchmark.phase != ThreadBenchmark::Phase::Idle && s_benchmark.phase != ThreadBenchmark::Phase::Done; bool benchDone = s_benchmark.phase == ThreadBenchmark::Phase::Done; ImFont* icoFont = Type().iconSmall(); if (benchRunning) { // Show progress bar + current test info float barW = std::min(180.0f * hs, idleRightEdge - (cardMin.x + pad) - 10.0f * dp); float barH = 4.0f * dp; float barX = idleRightEdge - barW; float barY = curY + headerH - barH - 2.0f * dp; // Progress bar track dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH), WithAlpha(OnSurface(), 30), barH * 0.5f); // Progress bar fill float pct = s_benchmark.progress(); dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW * pct, barY + barH), Primary(), barH * 0.5f); // Status text above bar int ct = s_benchmark.current_index < (int)s_benchmark.candidates.size() ? s_benchmark.candidates[s_benchmark.current_index] : 0; // Estimated remaining time (uses observed warmup for better accuracy) int remaining_tests = (int)s_benchmark.candidates.size() - s_benchmark.current_index; float elapsed_in_phase = s_benchmark.phase_timer; float phase_total; if (s_benchmark.phase == ThreadBenchmark::Phase::WarmingUp) phase_total = s_benchmark.avgWarmupSecs(); // adaptive estimate else if (s_benchmark.phase == ThreadBenchmark::Phase::CoolingDown) phase_total = ThreadBenchmark::COOLDOWN_SECS; else phase_total = ThreadBenchmark::MEASURE_SECS; float remaining_in_current = std::max(0.0f, phase_total - elapsed_in_phase); // Remaining tests after current each need warmup + measure + cooldown float est_secs = remaining_in_current + (remaining_tests - 1) * (s_benchmark.avgWarmupSecs() + ThreadBenchmark::MEASURE_SECS + ThreadBenchmark::COOLDOWN_SECS); int est_min = (int)(est_secs / 60.0f); int est_sec = (int)est_secs % 60; const char* phase_label; if (s_benchmark.phase == ThreadBenchmark::Phase::CoolingDown) phase_label = TR("mining_benchmark_cooling"); else if (s_benchmark.phase == ThreadBenchmark::Phase::WarmingUp && s_benchmark.phase_timer >= ThreadBenchmark::MIN_WARMUP_SECS) phase_label = TR("mining_benchmark_stabilizing"); else phase_label = TR("mining_benchmark_testing"); if (est_min > 0) snprintf(buf, sizeof(buf), "%s %d/%d (%dt) ~%dm%ds", phase_label, s_benchmark.current_index + 1, (int)s_benchmark.candidates.size(), ct, est_min, est_sec); else snprintf(buf, sizeof(buf), "%s %d/%d (%dt) ~%ds", phase_label, s_benchmark.current_index + 1, (int)s_benchmark.candidates.size(), ct, est_sec); ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(barX + (barW - txtSz.x) * 0.5f, barY - txtSz.y - 2.0f * dp), OnSurfaceMedium(), buf); // Cancel button (small X) float cancelSz = icoFont->LegacySize + 4.0f * dp; float cancelX = barX - cancelSz - 4.0f * dp; float cancelY = curY + (headerH - cancelSz) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(cancelX, cancelY)); ImGui::InvisibleButton("##BenchCancel", ImVec2(cancelSz, cancelSz)); if (ImGui::IsItemClicked()) { app->stopPoolMining(); if (s_benchmark.was_pool_running) app->startPoolMining(s_benchmark.prev_threads); s_benchmark.reset(); } if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); material::Tooltip("%s", TR("mining_benchmark_cancel")); } const char* cancelIcon = ICON_MD_CLOSE; ImVec2 cIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, cancelIcon); dl->AddText(icoFont, icoFont->LegacySize, ImVec2(cancelX + (cancelSz - cIcoSz.x) * 0.5f, cancelY + (cancelSz - cIcoSz.y) * 0.5f), OnSurfaceMedium(), cancelIcon); idleRightEdge = cancelX - 4.0f * dp; } else if (benchDone && s_benchmark.optimal_threads > 0) { // Show result briefly, then reset on next click snprintf(buf, sizeof(buf), "%s: %dt (%.1f H/s)", TR("mining_benchmark_result"), s_benchmark.optimal_threads, s_benchmark.optimal_hashrate); ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf); float txtX = idleRightEdge - txtSz.x; dl->AddText(capFont, capFont->LegacySize, ImVec2(txtX, curY + (headerH - txtSz.y) * 0.5f), WithAlpha(Success(), 220), buf); // Dismiss button float dismissSz = icoFont->LegacySize + 4.0f * dp; float dismissX = txtX - dismissSz - 4.0f * dp; float dismissY = curY + (headerH - dismissSz) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(dismissX, dismissY)); ImGui::InvisibleButton("##BenchDismiss", ImVec2(dismissSz, dismissSz)); if (ImGui::IsItemClicked()) s_benchmark.reset(); if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); material::Tooltip("%s", TR("mining_benchmark_dismiss")); } const char* okIcon = ICON_MD_CHECK; ImVec2 oIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, okIcon); dl->AddText(icoFont, icoFont->LegacySize, ImVec2(dismissX + (dismissSz - oIcoSz.x) * 0.5f, dismissY + (dismissSz - oIcoSz.y) * 0.5f), WithAlpha(Success(), 200), okIcon); idleRightEdge = dismissX - 4.0f * dp; } else if (s_pool_mode) { // Show benchmark button (only in pool mode) float btnSz = icoFont->LegacySize + 8.0f * dp; float btnX = idleRightEdge - btnSz; float btnY = curY + (headerH - btnSz) * 0.5f; material::IconButtonStyle st; st.color = OnSurfaceMedium(); st.hoverBg = StateHover(); st.tooltip = TR("mining_benchmark_tooltip"); ImGui::SetCursorScreenPos(ImVec2(btnX, btnY)); if (material::IconButton("##BenchStart", ICON_MD_SPEED, icoFont, ImVec2(btnSz, btnSz), st)) { // Require a wallet address for pool mining std::string worker(s_pool_worker); if (!worker.empty()) { static bool s_benchConfirm = false; if (!s_benchConfirm) { // First click: build candidates so we can estimate the total duration, then // require a confirming click (the benchmark interrupts mining and runs a while). s_benchmark.reset(); s_benchmark.buildCandidates(max_threads); s_benchConfirm = true; char msg[128]; snprintf(msg, sizeof(msg), TR("grpc_benchmark_takes_secs"), (int)(s_benchmark.totalEstimatedSecs() + 0.5f)); Notifications::instance().warning(msg); } else { // Second click: start (candidates already built above). s_benchConfirm = false; s_benchmark.was_pool_running = state.pool_mining.xmrig_running; s_benchmark.prev_threads = s_selected_threads; s_benchmark.phase = ThreadBenchmark::Phase::Starting; if (mining.generate) app->stopMining(); } } } idleRightEdge = btnX - 4.0f * dp; } // --- Miner update: "Update " button + current version (left of benchmark) --- if (s_pool_mode && !benchRunning) { // "Current" prefers the live version reported by the running miner (so a // bundled miner with no persisted release tag still shows a version). // Remembered for the session so it persists after the miner stops; falls // back to the persisted release tag (set by the updater), else "none". static std::string s_live_miner_ver; if (!state.pool_mining.version.empty()) s_live_miner_ver = state.pool_mining.version; // Live running version → detected installed version (before mining) → // persisted release tag (updater) → "none". std::string curVer = s_live_miner_ver; if (curVer.empty()) curVer = app->poolMiningInstalledVersion(); if (curVer.empty()) curVer = app->settings()->getXmrigVersion(); // No miner installed at all → pool mining can't run. Make this a prominent // "Download miner" call-to-action instead of a subtle grey "Update" pill. const bool minerMissing = curVer.empty(); // Version state: subtle GREEN + "xmrig releases" when up to date, subtle ORANGE + // "Update " when an update is available, neutral when either version is unknown. auto verNum = [](std::string s) -> std::vector { if (!s.empty() && (s[0] == 'v' || s[0] == 'V')) s.erase(0, 1); s = s.substr(0, s.find('-')); // drop any -build suffix std::vector v; size_t p = 0; while (p <= s.size()) { size_t q = s.find('.', p); v.push_back(std::atoi(s.substr(p, q == std::string::npos ? std::string::npos : q - p).c_str())); if (q == std::string::npos) break; p = q + 1; } return v; }; const bool verKnown = !curVer.empty() && !s_xmrig_latest_tag.empty(); bool upToDate = false; if (verKnown) { auto a = verNum(curVer), b = verNum(s_xmrig_latest_tag); int cmp = 0; for (size_t i = 0; i < std::max(a.size(), b.size()) && cmp == 0; ++i) { int x = i < a.size() ? a[i] : 0, y = i < b.size() ? b[i] : 0; if (x != y) cmp = x < y ? -1 : 1; } upToDate = (cmp >= 0); // installed >= latest } const bool outdated = verKnown && !upToDate; const ImU32 subtleGreen = IM_COL32(120, 190, 130, 255); const ImU32 subtleOrange = IM_COL32(214, 158, 74, 255); char xbtn[64]; if (minerMissing) snprintf(xbtn, sizeof(xbtn), "%s", TR("xmrig_download_miner")); else if (upToDate) snprintf(xbtn, sizeof(xbtn), "%s", TR("xmrig_releases")); else if (!s_xmrig_latest_tag.empty()) snprintf(xbtn, sizeof(xbtn), "%s %s", TR("xmrig_update_short"), s_xmrig_latest_tag.c_str()); else snprintf(xbtn, sizeof(xbtn), "%s", TR("xmrig_update_button")); const bool minerBusy = state.pool_mining.xmrig_running; ImVec2 xlblSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, xbtn); float xpadX = 8.0f * dp; float xbtnW = xlblSz.x + xpadX * 2.0f; float xbtnH = capFont->LegacySize + 6.0f * dp; float xbtnX = idleRightEdge - xbtnW; float xbtnY = curY + (headerH - xbtnH) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(xbtnX, xbtnY)); ImGui::InvisibleButton("##XmrigUpdateBtn", ImVec2(xbtnW, xbtnH)); bool xhov = ImGui::IsItemHovered(); bool xclk = ImGui::IsItemClicked(); ImU32 xpill = minerBusy ? WithAlpha(OnSurface(), 12) : (minerMissing ? WithAlpha(Primary(), xhov ? 70 : 45) : (xhov ? StateHover() : WithAlpha(OnSurface(), 28))); dl->AddRectFilled(ImVec2(xbtnX, xbtnY), ImVec2(xbtnX + xbtnW, xbtnY + xbtnH), xpill, xbtnH * 0.3f); dl->AddText(capFont, capFont->LegacySize, ImVec2(xbtnX + xpadX, xbtnY + (xbtnH - xlblSz.y) * 0.5f), minerBusy ? OnSurfaceDisabled() : (minerMissing ? Primary() : (upToDate ? subtleGreen : outdated ? subtleOrange : OnSurfaceMedium())), xbtn); if (xhov) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); material::Tooltip("%s", minerBusy ? TR("xmrig_stop_mining_first") : (minerMissing ? TR("xmrig_download_miner_tip") : TR("xmrig_update_title"))); } if (xclk && !minerBusy) XmrigDownloadDialog::show(app); // Current installed version, as text to the LEFT of the button. char xcur[64]; snprintf(xcur, sizeof(xcur), "%s %s", TR("xmrig_current"), curVer.empty() ? TR("xmrig_none") : curVer.c_str()); ImVec2 xcurSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, xcur); float xcurX = xbtnX - 6.0f * dp - xcurSz.x; dl->AddText(capFont, capFont->LegacySize, ImVec2(xcurX, curY + (headerH - xcurSz.y) * 0.5f), upToDate ? subtleGreen : outdated ? subtleOrange : OnSurfaceDisabled(), xcur); idleRightEdge = xcurX - 8.0f * dp; } ImGui::SetCursorScreenPos(benchSavedCur); } // Active mining indicator (left of idle toggle) if (mining.generate) { float pulse = effects::isLowSpecMode() ? schema::UI().drawElement("animations", "pulse-base-normal").size : schema::UI().drawElement("animations", "pulse-base-normal").size + schema::UI().drawElement("animations", "pulse-amp-normal").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-normal").size); ImU32 pulseCol = WithAlpha(Success(), (int)(255 * pulse)); float dotR = schema::UI().drawElement("tabs.mining", "active-dot-radius").size + 2.0f * hs; dl->AddCircleFilled(ImVec2(idleRightEdge - dotR, curY + dotR + 1 * dp), dotR, pulseCol); dl->AddText(capFont, capFont->LegacySize, ImVec2(idleRightEdge - dotR - dotR - 60 * hs, curY), WithAlpha(Success(), 200), TR("mining_active")); } curY += headerH + secGap; } // --- Thread Grid (drag-to-select) --- { // Center the grid horizontally within the card float gridX = cardMin.x + pad + (innerW - gridW) * 0.5f; float gridY = curY; bool threads_changed = false; // Track which thread the mouse is currently over (-1 = none) int hovered_thread = -1; // First pass: hit-test all cells to find the hovered thread-count option (its value, not index) for (int i = 0; i < nOpts; i++) { int row = i / cols; int col = i % cols; float cx = gridX + col * (cellSz + cellGap); float cy = gridY + row * (cellSz + cellGap); if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + cellSz, cy + cellSz))) { hovered_thread = threadOptions[i]; break; } } // Show pointer cursor when hovering the thread grid bool benchActive = s_benchmark.phase != ThreadBenchmark::Phase::Idle && s_benchmark.phase != ThreadBenchmark::Phase::Done; if (hovered_thread > 0 && !benchActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); // Drag-to-select logic (disabled during benchmark; ignored while a dropdown popup is open) if (!benchActive && !anyPopupOpen && ImGui::IsMouseClicked(0) && hovered_thread > 0) { // Begin drag. A click on the tile that is already selected changes nothing (it used to save // settings and restart the miner). s_drag_active = true; s_drag_anchor_thread = hovered_thread; if (hovered_thread != s_selected_threads) { s_selected_threads = hovered_thread; threads_changed = true; } } if (s_drag_active) { if (ImGui::IsMouseDown(0)) { if (hovered_thread > 0 && hovered_thread != s_selected_threads) { // Drag selects up to the hovered thread in either direction s_selected_threads = hovered_thread; threads_changed = true; } } else { // Mouse released — end drag s_drag_active = false; } } // Render cells (one per discrete thread-count option) for (int i = 0; i < nOpts; i++) { int row = i / cols; int col = i % cols; float cx = gridX + col * (cellSz + cellGap); float cy = gridY + row * (cellSz + cellGap); ImVec2 cMin(cx, cy); ImVec2 cMax(cx + cellSz, cy + cellSz); int threadNum = threadOptions[i]; bool active = threadNum <= s_selected_threads; // fill-up-to-selected heat metaphor bool hovered = (threadNum == hovered_thread); // Determine visual state float rounding = schema::UI().drawElement("tabs.mining", "cell-rounding").size; if (active && mining.generate) { // Mining + active: animated heat glow using theme colors float t = (float)ImGui::GetTime(); float phase = t * schema::UI().drawElement("animations", "heat-glow-speed").size + i * schema::UI().drawElement("animations", "heat-glow-phase-offset").size; float glow = effects::isLowSpecMode() ? 0.5f : 0.5f + 0.5f * (float)std::sin(phase); // Base color from theme (--mining-heat-glow) falling back to Primary ImU32 heatBase = schema::UI().resolveColor("var(--mining-heat-glow)", Primary()); int baseR = (heatBase >> 0) & 0xFF; int baseG = (heatBase >> 8) & 0xFF; int baseB = (heatBase >> 16) & 0xFF; // Brighten toward white as glow increases int r = std::min(255, (int)(baseR + (255 - baseR) * 0.3f * glow)); int g = std::min(255, (int)(baseG + (255 - baseG) * 0.3f * glow)); int b = std::min(255, (int)(baseB + (255 - baseB) * 0.3f * glow)); int a = (int)(180 + 60 * glow); ImU32 fillCol = IM_COL32(r, g, b, a); dl->AddRectFilled(cMin, cMax, fillCol, rounding); // Bright border dl->AddRect(cMin, cMax, WithAlpha(Primary(), (int)(160 + 60 * glow)), rounding, 0, schema::UI().drawElement("tabs.mining", "active-cell-border-thickness").size); } else if (active) { // Active but not mining: solid primary fill ImU32 fillCol = material::WithAlpha(Primary(), hovered ? 220 : 180); dl->AddRectFilled(cMin, cMax, fillCol, rounding); dl->AddRect(cMin, cMax, material::WithAlpha(Primary(), 255), rounding, 0, schema::UI().drawElement("tabs.mining", "cell-border-thickness").size); } else { // Inactive: dim outline ImU32 fillCol = hovered ? WithAlpha(OnSurface(), 25) : WithAlpha(OnSurface(), 8); dl->AddRectFilled(cMin, cMax, fillCol, rounding); dl->AddRect(cMin, cMax, WithAlpha(OnSurface(), hovered ? 80 : 35), rounding); } // Thread number label (centered) snprintf(buf, sizeof(buf), "%d", threadNum); ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf); ImVec2 txtPos(cx + (cellSz - txtSz.x) * 0.5f, cy + (cellSz - txtSz.y) * 0.5f); ImU32 txtCol = active ? schema::UI().resolveColor("var(--on-primary)", IM_COL32(255, 255, 255, 230)) : WithAlpha(OnSurface(), 80); dl->AddText(capFont, capFont->LegacySize, txtPos, txtCol, buf); } // Report every change (the header, RAM estimate and tile fill follow s_selected_threads live), but // let the App apply it: it holds the commit while the mouse is down, so a drag across 16 tiles is // ONE settings save and at most ONE miner change, on release. if (threads_changed) app->selectMiningThreads(s_selected_threads); app->setMiningThreadDragActive(s_drag_active); curY += gridH + secGap; } // ============================================================ // Large square mining button — right of the controls card // ============================================================ { float btnX = cardMin.x + controlsW + miningBtnGap; float btnY = cardMin.y; ImVec2 bMin(btnX, btnY); ImVec2 bMax(btnX + miningBtnSz, btnY + cardH); bool btnHovered = !anyPopupOpen && material::IsRectHovered(bMin, bMax); // don't bleed through an open dropdown bool btnClicked = btnHovered && ImGui::IsMouseClicked(0); bool isSyncing = state.sync.syncing; bool poolBlockedBySolo = s_pool_mode && mining.generate && !state.pool_mining.xmrig_running; bool isToggling = app->isMiningToggleInProgress(); // Pool mining connects to an external pool via xmrig — it does not // need the local blockchain synced or even the daemon connected. // If pool mining is still shutting down after switching to solo, // keep the button enabled so user can stop it. bool poolStillRunning = !s_pool_mode && state.pool_mining.xmrig_running; // Can't start pool mining without a payout address (blank for a new wallet with no z-address); // only blocks starting — stopping a running miner stays enabled. // Block start when the payout address is empty OR not a valid DragonX address — mining to a // malformed / wrong-chain address silently loses the rewards. (M-01) const std::string poolPayoutStr(s_pool_worker); bool poolNeedsPayout = s_pool_mode && !state.pool_mining.xmrig_running && (poolPayoutStr.empty() || (poolPayoutStr != "x" && !util::isValidRecipientAddress(poolPayoutStr))); bool disabled = s_pool_mode ? (isToggling || poolBlockedBySolo || poolNeedsPayout) : (poolStillRunning ? false : (!app->isConnected() || isToggling || isSyncing)); // Glass panel background with state-dependent tint GlassPanelSpec btnGlass; btnGlass.rounding = Layout::glassRounding(); if (isToggling) { // Toggling: subtle pulsing glow to indicate activity float pulse = effects::isLowSpecMode() ? 0.5f : 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 4.0); int glowA = (int)(15 + 25 * pulse); btnGlass.fillAlpha = glowA; } else if (isMiningActive) { // Active mining: warm glow float pulse = effects::isLowSpecMode() ? schema::UI().drawElement("animations", "pulse-base-glow").size : schema::UI().drawElement("animations", "pulse-base-glow").size + schema::UI().drawElement("animations", "pulse-amp-glow").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-slow").size); int glowA = (int)(20 + 30 * pulse); btnGlass.fillAlpha = glowA; } else { btnGlass.fillAlpha = btnHovered ? 30 : 18; } DrawGlassPanel(dl, bMin, bMax, btnGlass); // Hover highlight if (btnHovered && !disabled) { dl->AddRectFilled(bMin, bMax, isMiningActive ? WithAlpha(Error(), 25) : WithAlpha(Success(), 20), btnGlass.rounding); } // Draw mining icon centered in button — Material Design ICON_MD_CONSTRUCTION { float btnW = bMax.x - bMin.x; float btnH = bMax.y - bMin.y; float cx = bMin.x + btnW * 0.5f; float cy = bMin.y + btnH * schema::UI().drawElement("tabs.mining", "button-icon-y-ratio").size; // shift up to leave room for label if (isToggling) { // Draw a spinning arc spinner to indicate progress float spinnerR = std::min(btnW, btnH) * 0.18f; float thickness = std::max(2.5f, spinnerR * 0.15f); float time = (float)ImGui::GetTime(); // Track circle (faint) ImU32 trackCol = WithAlpha(Primary(), 40); dl->AddCircle(ImVec2(cx, cy), spinnerR, trackCol, 0, thickness); // Animated arc float rotation = fmodf(time * 2.0f * IM_PI / 1.4f, IM_PI * 2.0f); float cycleTime = fmodf(time, 1.333f); float arcLength = (cycleTime < 0.666f) ? (cycleTime / 0.666f) * 0.75f + 0.1f : ((1.333f - cycleTime) / 0.666f) * 0.75f + 0.1f; float startAngle = rotation - IM_PI * 0.5f; float endAngle = startAngle + IM_PI * 2.0f * arcLength; int segments = (int)(32 * arcLength) + 1; float angleStep = (endAngle - startAngle) / segments; ImU32 arcCol = Primary(); for (int i = 0; i < segments; i++) { float a1 = startAngle + angleStep * i; float a2 = startAngle + angleStep * (i + 1); ImVec2 p1(cx + cosf(a1) * spinnerR, cy + sinf(a1) * spinnerR); ImVec2 p2(cx + cosf(a2) * spinnerR, cy + sinf(a2) * spinnerR); dl->AddLine(p1, p2, arcCol, thickness); } } else { ImU32 iconCol; if (disabled) { iconCol = OnSurfaceDisabled(); } else if (isMiningActive) { float pulse = effects::isLowSpecMode() ? schema::UI().drawElement("animations", "pulse-base-normal").size : schema::UI().drawElement("animations", "pulse-base-normal").size + schema::UI().drawElement("animations", "pulse-amp-normal").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-normal").size); iconCol = WithAlpha(Error(), (int)(200 + 55 * pulse)); } else { iconCol = btnHovered ? WithAlpha(Success(), 255) : OnSurfaceMedium(); } // Use XL icon for the large mining button ImFont* iconFont = Type().iconXL(); const char* mineIcon = isMiningActive ? ICON_MD_CLOSE : ICON_MD_CONSTRUCTION; ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, mineIcon); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx - iconSz.x * 0.5f, cy - iconSz.y * 0.5f), iconCol, mineIcon); } } // Label below icon { float btnW = bMax.x - bMin.x; float btnH = bMax.y - bMin.y; const char* label; ImU32 lblCol; if (isToggling) { label = isMiningActive ? TR("mining_stopping") : TR("mining_starting"); // Animated dots effect via alpha pulse float pulse = effects::isLowSpecMode() ? 0.7f : 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 3.0); lblCol = WithAlpha(Primary(), (int)(120 + 135 * pulse)); } else if (isMiningActive) { label = TR("mining_stop"); lblCol = WithAlpha(Error(), 220); } else if (disabled) { label = TR("mining_mine"); lblCol = WithAlpha(OnSurface(), 50); } else { label = TR("mining_mine"); lblCol = WithAlpha(OnSurface(), 160); } ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label); float lblX = bMin.x + (btnW - lblSz.x) * 0.5f; float lblY = bMin.y + btnH * schema::UI().drawElement("tabs.mining", "button-label-y-ratio").size; dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lblX, lblY), lblCol, label); } // Tooltip + pointer cursor if (btnHovered) { if (!disabled) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (isToggling) material::Tooltip("%s", isMiningActive ? TR("mining_stopping_tooltip") : TR("mining_starting_tooltip")); else if (isSyncing && !s_pool_mode) material::Tooltip(TR("mining_syncing_tooltip"), state.sync.verification_progress * 100.0); else if (!s_pool_mode && !poolStillRunning && !app->isConnected()) material::Tooltip("%s", TR("mining_not_connected_tooltip")); // was falling through to "Start Mining" on a dead button else if (poolBlockedBySolo) material::Tooltip("%s", TR("mining_stop_solo_for_pool")); else if (poolNeedsPayout) material::Tooltip("%s", util::isTransparentAddress(poolPayoutStr) ? TR("mining_payout_needs_shielded") : TR("mining_pool_needs_payout_tooltip")); else material::Tooltip("%s", isMiningActive ? TR("stop_mining") : TR("start_mining")); } // Click action — pool or solo if (btnClicked && !disabled) { if (s_pool_mode) { if (state.pool_mining.xmrig_running) app->stopPoolMining(); else app->startPoolMining(s_selected_threads); } else { // If pool mining is still running (user just switched from pool to solo), // stop pool mining first if (state.pool_mining.xmrig_running) app->stopPoolMining(); else if (mining.generate) app->stopMining(); else app->startMining(s_selected_threads); } } } ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); // Persistent error caption (a crashed/AV-blocked/failed miner otherwise leaves a button that // "did nothing" once its toast faded). Normal ImGui flow, OUTSIDE the hand-drawn button card. const std::string& merr = app->miningLastError(); if (!merr.empty() && !app->isMiningToggleInProgress()) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::PushTextWrapPos(cardMin.x + availWidth); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Error())); ImGui::TextWrapped("%s", merr.c_str()); ImGui::PopStyleColor(); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium())); ImGui::TextUnformatted(TR("mining_error_retry_hint")); ImGui::PopStyleColor(); ImGui::PopTextWrapPos(); #ifdef _WIN32 if (app->miningErrorIsAntivirus()) { ImGui::SameLine(0, Layout::spacingSm()); if (ImGui::SmallButton(TR("mining_error_av_help"))) app->showAntivirusHelp(); } #endif ImGui::SameLine(0, Layout::spacingSm()); if (ImGui::SmallButton(TR("mining_error_dismiss"))) app->clearMiningError(); } ImGui::Dummy(ImVec2(0, gap)); } } // namespace ui } // namespace dragonx