diff options
Diffstat (limited to 'src/gui.cpp')
| -rw-r--r-- | src/gui.cpp | 766 |
1 files changed, 766 insertions, 0 deletions
diff --git a/src/gui.cpp b/src/gui.cpp new file mode 100644 index 0000000..6d81b9f --- /dev/null +++ b/src/gui.cpp | |||
| @@ -0,0 +1,766 @@ | |||
| 1 | // Bitflash GUI — ImGui + GLFW + OpenGL3 | ||
| 2 | // Same source on Linux and Windows. No platform ifdefs. | ||
| 3 | |||
| 4 | #include "imgui/imgui.h" | ||
| 5 | #include "imgui/imgui_impl_glfw.h" | ||
| 6 | #include "imgui/imgui_impl_opengl3.h" | ||
| 7 | #include <GLFW/glfw3.h> | ||
| 8 | #include "headers_core.h" | ||
| 9 | #include <string> | ||
| 10 | #include <vector> | ||
| 11 | #include <ctime> | ||
| 12 | #include <cstdio> | ||
| 13 | #include <sstream> | ||
| 14 | #include <iomanip> | ||
| 15 | #include "font_roboto.h" | ||
| 16 | |||
| 17 | // --------------------------------------------------------------------------- | ||
| 18 | // Node API | ||
| 19 | // --------------------------------------------------------------------------- | ||
| 20 | extern int64 nTransactionFee; | ||
| 21 | extern int fGenerateBitcoins; | ||
| 22 | extern int nBestHeight; | ||
| 23 | extern int nMineMode; | ||
| 24 | extern std::string strParticipantPool; | ||
| 25 | extern CAddress addrLocalHost; | ||
| 26 | |||
| 27 | void MainFrameRepaint(); | ||
| 28 | string DateTimeStr(int64 nTime); | ||
| 29 | bool SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew); | ||
| 30 | int64 GetBalance(); | ||
| 31 | string PubKeyToAddress(const std::vector<unsigned char>& vchPubKey); | ||
| 32 | bool AddressToHash160(const std::string& str, uint160& hash160Ret); | ||
| 33 | void ThreadBitcoinMiner(void*); | ||
| 34 | void ThreadRPCServer(void*); | ||
| 35 | |||
| 36 | // --------------------------------------------------------------------------- | ||
| 37 | // State | ||
| 38 | // --------------------------------------------------------------------------- | ||
| 39 | static std::string g_myAddress; | ||
| 40 | static int64 g_balance = 0; | ||
| 41 | static int64 g_lastWalletRefresh = 0; | ||
| 42 | static bool g_showSend = false; | ||
| 43 | static bool g_showOptions = false; | ||
| 44 | static bool g_showAbout = false; | ||
| 45 | static bool g_needRefresh = true; | ||
| 46 | |||
| 47 | static char g_sendAddr[128] = {}; | ||
| 48 | static char g_sendAmount[32] = {}; | ||
| 49 | static std::string g_sendStatus; | ||
| 50 | static char g_participantPool[256] = {}; | ||
| 51 | static char g_poolName[128] = {}; | ||
| 52 | static char g_poolFee[32] = {}; | ||
| 53 | static char g_poolDash[256] = {}; | ||
| 54 | static int g_mineRadio = 0; | ||
| 55 | |||
| 56 | struct TxRow { std::string date, desc; int64 amount; int depth; }; | ||
| 57 | static std::vector<TxRow> g_txRows; | ||
| 58 | |||
| 59 | // --------------------------------------------------------------------------- | ||
| 60 | // Helpers | ||
| 61 | // --------------------------------------------------------------------------- | ||
| 62 | static std::string FmtMoney(int64 n) | ||
| 63 | { | ||
| 64 | bool neg = n < 0; if (neg) n = -n; | ||
| 65 | char buf[64]; | ||
| 66 | snprintf(buf, sizeof(buf), "%s%lld.%08lld", | ||
| 67 | neg?"-":"", (long long)(n/COIN), (long long)(n%COIN)); | ||
| 68 | char* dot = strchr(buf, '.'); | ||
| 69 | if (dot) { char* e = buf+strlen(buf)-1; while(e>dot+2&&*e=='0') *e--='\0'; } | ||
| 70 | return buf; | ||
| 71 | } | ||
| 72 | |||
| 73 | static std::string FmtAge(int64 createdAt) | ||
| 74 | { | ||
| 75 | int64 age = GetTime() - createdAt; | ||
| 76 | if (age < 0) | ||
| 77 | age = 0; | ||
| 78 | if (age < 60) | ||
| 79 | return strprintf("%llds", (long long)age); | ||
| 80 | if (age < 3600) | ||
| 81 | return strprintf("%lldm", (long long)(age / 60)); | ||
| 82 | return strprintf("%lldh", (long long)(age / 3600)); | ||
| 83 | } | ||
| 84 | |||
| 85 | static ImVec4 ParticipantStatusColor(const std::string& status) | ||
| 86 | { | ||
| 87 | if (status.find("failed") != std::string::npos || | ||
| 88 | status.find("rejected") != std::string::npos || | ||
| 89 | status.find("stopped") != std::string::npos) | ||
| 90 | return ImVec4(1.0f, 0.38f, 0.38f, 1.0f); | ||
| 91 | if (status.find("hashing") != std::string::npos || | ||
| 92 | status.find("accepted") != std::string::npos || | ||
| 93 | status.find("connected") != std::string::npos) | ||
| 94 | return ImVec4(0.35f, 1.0f, 0.45f, 1.0f); | ||
| 95 | if (status.find("asking") != std::string::npos || | ||
| 96 | status.find("authoriz") != std::string::npos || | ||
| 97 | status.find("subscrib") != std::string::npos) | ||
| 98 | return ImVec4(1.0f, 0.75f, 0.25f, 1.0f); | ||
| 99 | return ImVec4(0.55f, 0.80f, 1.0f, 1.0f); | ||
| 100 | } | ||
| 101 | |||
| 102 | static std::string PoolLabel(const BtfPoolAnnouncement& ann) | ||
| 103 | { | ||
| 104 | std::ostringstream o; | ||
| 105 | o << ann.poolName << " - " << std::fixed << std::setprecision(2) << ann.feePercent << "%"; | ||
| 106 | if (ann.connectedMiners > 0) | ||
| 107 | o << " - " << ann.connectedMiners << " miners"; | ||
| 108 | return o.str(); | ||
| 109 | } | ||
| 110 | |||
| 111 | static void RefreshWallet() | ||
| 112 | { | ||
| 113 | std::vector<unsigned char> vchPubKey; | ||
| 114 | if (CWalletDB("r").ReadDefaultKey(vchPubKey)) | ||
| 115 | g_myAddress = PubKeyToAddress(vchPubKey); | ||
| 116 | g_balance = GetBalance(); | ||
| 117 | |||
| 118 | g_txRows.clear(); | ||
| 119 | TRY_CRITICAL_BLOCK(cs_mapWallet) | ||
| 120 | { | ||
| 121 | std::vector<std::pair<int64,uint256>> vs; | ||
| 122 | for (auto& kv : mapWallet) vs.push_back({kv.second.GetTxTime(), kv.first}); | ||
| 123 | std::sort(vs.rbegin(), vs.rend()); | ||
| 124 | for (auto& sv : vs) { | ||
| 125 | auto mi = mapWallet.find(sv.second); | ||
| 126 | if (mi == mapWallet.end()) continue; | ||
| 127 | CWalletTx& wtx = mi->second; | ||
| 128 | int64 net = wtx.GetCredit() - wtx.GetDebit(); | ||
| 129 | int depth = wtx.GetDepthInMainChain(); | ||
| 130 | std::string desc; | ||
| 131 | if (wtx.IsCoinBase()) desc = depth<1 ? "Generated (unconfirmed)" : "Generated"; | ||
| 132 | else if (wtx.GetDebit() > 0) desc = "Sent"; | ||
| 133 | else desc = "Received"; | ||
| 134 | g_txRows.push_back({DateTimeStr(wtx.GetTxTime()), desc, net, depth}); | ||
| 135 | } | ||
| 136 | } | ||
| 137 | g_lastWalletRefresh = GetTime(); | ||
| 138 | } | ||
| 139 | |||
| 140 | void MainFrameRepaint() { g_needRefresh = true; } | ||
| 141 | |||
| 142 | string DateTimeStr(int64 nTime) | ||
| 143 | { | ||
| 144 | time_t t = (time_t)nTime; | ||
| 145 | struct tm* p = localtime(&t); | ||
| 146 | if (!p) return ""; | ||
| 147 | char buf[32]; strftime(buf, sizeof(buf), "%m/%d/%y %H:%M", p); | ||
| 148 | return buf; | ||
| 149 | } | ||
| 150 | |||
| 151 | // --------------------------------------------------------------------------- | ||
| 152 | // Main window | ||
| 153 | // --------------------------------------------------------------------------- | ||
| 154 | static void DrawMainWindow() | ||
| 155 | { | ||
| 156 | ImGuiIO& io = ImGui::GetIO(); | ||
| 157 | ImGui::SetNextWindowPos(ImVec2(0,0)); | ||
| 158 | ImGui::SetNextWindowSize(io.DisplaySize); | ||
| 159 | ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12,12)); | ||
| 160 | ImGui::Begin("##main", nullptr, | ||
| 161 | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | | ||
| 162 | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_MenuBar | | ||
| 163 | ImGuiWindowFlags_NoFocusOnAppearing); | ||
| 164 | ImGui::SetWindowFontScale(0.92f); | ||
| 165 | ImGui::PopStyleVar(); | ||
| 166 | |||
| 167 | // ---- Menu bar ---- | ||
| 168 | if (ImGui::BeginMenuBar()) { | ||
| 169 | if (ImGui::BeginMenu("File")) { | ||
| 170 | if (ImGui::MenuItem("Exit")) glfwSetWindowShouldClose(glfwGetCurrentContext(), true); | ||
| 171 | ImGui::EndMenu(); | ||
| 172 | } | ||
| 173 | if (ImGui::MenuItem("Options")) g_showOptions = true; | ||
| 174 | if (ImGui::MenuItem("About")) g_showAbout = true; | ||
| 175 | ImGui::EndMenuBar(); | ||
| 176 | } | ||
| 177 | |||
| 178 | // ---- Address bar ---- | ||
| 179 | ImGui::Spacing(); | ||
| 180 | ImGui::TextDisabled("Address:"); | ||
| 181 | ImGui::SameLine(); | ||
| 182 | ImGui::SetNextItemWidth(-80); | ||
| 183 | ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.14f,0.14f,0.14f,1)); | ||
| 184 | ImGui::InputText("##addr", (char*)g_myAddress.c_str(), g_myAddress.size()+1, | ||
| 185 | ImGuiInputTextFlags_ReadOnly); | ||
| 186 | ImGui::PopStyleColor(); | ||
| 187 | ImGui::SameLine(); | ||
| 188 | if (ImGui::Button("Copy##a")) ImGui::SetClipboardText(g_myAddress.c_str()); | ||
| 189 | |||
| 190 | // ---- Balance on its own line ---- | ||
| 191 | ImGui::TextDisabled("Balance:"); | ||
| 192 | ImGui::SameLine(); | ||
| 193 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.35f,1.0f,0.35f,1.0f)); | ||
| 194 | ImGui::Text("%s BTF", FmtMoney(g_balance).c_str()); | ||
| 195 | ImGui::PopStyleColor(); | ||
| 196 | |||
| 197 | // ---- Toolbar ---- | ||
| 198 | ImGui::Spacing(); | ||
| 199 | ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f,0.40f,0.15f,1)); | ||
| 200 | ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.20f,0.55f,0.20f,1)); | ||
| 201 | if (ImGui::Button(" Send Coins ")) g_showSend = true; | ||
| 202 | ImGui::PopStyleColor(2); | ||
| 203 | |||
| 204 | // Mining status indicator | ||
| 205 | ImGui::SameLine(0, 20); | ||
| 206 | if (fGenerateBitcoins) { | ||
| 207 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f,0.6f,0.0f,1.0f)); | ||
| 208 | const char* modeLabel = nMineMode==MINE_OPERATOR ? "Mining (operator)" : | ||
| 209 | nMineMode==MINE_PARTICIPANT ? "Mining (pool)" : "Mining"; | ||
| 210 | ImGui::Text("⬤ %s", modeLabel); | ||
| 211 | ImGui::PopStyleColor(); | ||
| 212 | if (nMineMode == MINE_PARTICIPANT) { | ||
| 213 | std::string mineStatus = GetParticipantMiningStatus(); | ||
| 214 | if (!mineStatus.empty()) { | ||
| 215 | ImGui::SameLine(0, 12); | ||
| 216 | ImGui::PushStyleColor(ImGuiCol_Text, ParticipantStatusColor(mineStatus)); | ||
| 217 | ImGui::Text("[%s]", mineStatus.c_str()); | ||
| 218 | ImGui::PopStyleColor(); | ||
| 219 | } | ||
| 220 | } | ||
| 221 | } else { | ||
| 222 | ImGui::TextDisabled("○ Not mining"); | ||
| 223 | } | ||
| 224 | |||
| 225 | ImGui::Spacing(); | ||
| 226 | ImGui::Separator(); | ||
| 227 | ImGui::Spacing(); | ||
| 228 | |||
| 229 | // ---- Live telemetry strip ---- | ||
| 230 | { | ||
| 231 | const char* modeName = nMineMode==MINE_OPERATOR ? "Operator" : | ||
| 232 | nMineMode==MINE_PARTICIPANT ? "Participant" : "Solo"; | ||
| 233 | int64 age = g_lastWalletRefresh > 0 ? (GetTime() - g_lastWalletRefresh) : -1; | ||
| 234 | if (age < 0) age = 0; | ||
| 235 | |||
| 236 | ImGui::Text("Mode: %s", modeName); | ||
| 237 | ImGui::SameLine(0, 16); | ||
| 238 | ImGui::Text("Wallet refresh: %llds ago", (long long)age); | ||
| 239 | |||
| 240 | if (nMineMode == MINE_PARTICIPANT) { | ||
| 241 | uint64 sent = 0, accepted = 0; | ||
| 242 | double hashRate = 0.0; | ||
| 243 | std::string mineStatus = GetParticipantMiningStatus(); | ||
| 244 | GetParticipantMiningStats(sent, accepted, hashRate); | ||
| 245 | if (sent > 0 || accepted > 0 || hashRate > 0.0) { | ||
| 246 | double acceptPct = sent ? (100.0 * (double)accepted / (double)sent) : 0.0; | ||
| 247 | ImGui::TextDisabled("Your mining: sent %llu, accepted %llu (%.1f%%), %.2f H/s", | ||
| 248 | (unsigned long long)sent, | ||
| 249 | (unsigned long long)accepted, | ||
| 250 | acceptPct, | ||
| 251 | hashRate); | ||
| 252 | } else if (!mineStatus.empty()) { | ||
| 253 | ImGui::TextDisabled("Your mining: %s", mineStatus.c_str()); | ||
| 254 | } else { | ||
| 255 | ImGui::TextDisabled("Your mining: asking pool for work"); | ||
| 256 | } | ||
| 257 | } else if (nMineMode == MINE_OPERATOR) { | ||
| 258 | int miners = 0, blocksFound = 0; | ||
| 259 | uint64 roundShares = 0; | ||
| 260 | GetPoolOperatorStats(miners, blocksFound, roundShares); | ||
| 261 | if (miners > 0 || blocksFound > 0 || roundShares > 0) | ||
| 262 | ImGui::TextDisabled("Pool runtime: %d authorized miners, %llu round shares, %d blocks found", miners, (unsigned long long)roundShares, blocksFound); | ||
| 263 | else | ||
| 264 | ImGui::TextDisabled("Pool runtime: waiting for miners"); | ||
| 265 | } else { | ||
| 266 | ImGui::TextDisabled("Solo runtime: local wallet mining telemetry"); | ||
| 267 | } | ||
| 268 | ImGui::Spacing(); | ||
| 269 | ImGui::Separator(); | ||
| 270 | ImGui::Spacing(); | ||
| 271 | } | ||
| 272 | |||
| 273 | // ---- Transaction list ---- | ||
| 274 | float listHeight = io.DisplaySize.y * 0.26f; | ||
| 275 | if (listHeight < 190.0f) listHeight = 190.0f; | ||
| 276 | if (listHeight > 260.0f) listHeight = 260.0f; | ||
| 277 | if (ImGui::BeginTable("txlist", 4, | ||
| 278 | ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | | ||
| 279 | ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp, | ||
| 280 | ImVec2(0, listHeight))) | ||
| 281 | { | ||
| 282 | ImGui::TableSetupScrollFreeze(0, 1); | ||
| 283 | ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 110); | ||
| 284 | ImGui::TableSetupColumn("Date", ImGuiTableColumnFlags_WidthFixed, 130); | ||
| 285 | ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch); | ||
| 286 | ImGui::TableSetupColumn("Amount", ImGuiTableColumnFlags_WidthFixed, 140); | ||
| 287 | ImGui::TableHeadersRow(); | ||
| 288 | |||
| 289 | for (auto& row : g_txRows) { | ||
| 290 | ImGui::TableNextRow(); | ||
| 291 | ImGui::TableSetColumnIndex(0); | ||
| 292 | if (row.depth < 1) | ||
| 293 | ImGui::TextColored(ImVec4(1,0.6f,0,1), "Unconfirmed"); | ||
| 294 | else | ||
| 295 | ImGui::Text("%d blocks deep", row.depth); | ||
| 296 | ImGui::TableSetColumnIndex(1); | ||
| 297 | ImGui::TextDisabled("%s", row.date.c_str()); | ||
| 298 | ImGui::TableSetColumnIndex(2); | ||
| 299 | ImGui::TextUnformatted(row.desc.c_str()); | ||
| 300 | ImGui::TableSetColumnIndex(3); | ||
| 301 | if (row.amount >= 0) | ||
| 302 | ImGui::TextColored(ImVec4(0.35f,1,0.35f,1), "+%s BTF", FmtMoney(row.amount).c_str()); | ||
| 303 | else | ||
| 304 | ImGui::TextColored(ImVec4(1,0.4f,0.4f,1), "%s BTF", FmtMoney(row.amount).c_str()); | ||
| 305 | } | ||
| 306 | ImGui::EndTable(); | ||
| 307 | } | ||
| 308 | |||
| 309 | std::vector<BtfPoolAnnouncement> pools; | ||
| 310 | BtfGetPoolAnnouncements(pools); | ||
| 311 | |||
| 312 | if (nMineMode == MINE_PARTICIPANT) { | ||
| 313 | ImGui::Spacing(); | ||
| 314 | ImGui::SeparatorText("Participant Mining"); | ||
| 315 | ImGui::Text("Selected pool:"); | ||
| 316 | if (strParticipantPool.empty()) | ||
| 317 | ImGui::TextDisabled("No pool selected yet"); | ||
| 318 | else | ||
| 319 | ImGui::TextWrapped("%s", strParticipantPool.c_str()); | ||
| 320 | |||
| 321 | const BtfPoolAnnouncement* selected = NULL; | ||
| 322 | for (size_t i = 0; i < pools.size(); i++) | ||
| 323 | if (pools[i].btfAddress == strParticipantPool) { selected = &pools[i]; break; } | ||
| 324 | |||
| 325 | if (selected) { | ||
| 326 | ImGui::Text("Pool name: %s", selected->poolName.c_str()); | ||
| 327 | ImGui::Text("Fee: %.2f%%", selected->feePercent); | ||
| 328 | ImGui::Text("Live stats: %d miners, %d blocks", selected->connectedMiners, selected->blocksFound); | ||
| 329 | if (!selected->dashboardUrl.empty()) | ||
| 330 | ImGui::TextWrapped("Dashboard: %s", selected->dashboardUrl.c_str()); | ||
| 331 | } else if (!strParticipantPool.empty()) { | ||
| 332 | ImGui::TextDisabled("No fresh announcement found for this pool yet"); | ||
| 333 | } | ||
| 334 | |||
| 335 | if (!pools.empty()) { | ||
| 336 | ImGui::Spacing(); | ||
| 337 | ImGui::SeparatorText("Live Pools"); | ||
| 338 | if (ImGui::BeginTable("livepools", 5, | ||
| 339 | ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) | ||
| 340 | { | ||
| 341 | ImGui::TableSetupColumn("Pool", ImGuiTableColumnFlags_WidthStretch); | ||
| 342 | ImGui::TableSetupColumn("Fee", ImGuiTableColumnFlags_WidthFixed, 70); | ||
| 343 | ImGui::TableSetupColumn("Stats", ImGuiTableColumnFlags_WidthFixed, 160); | ||
| 344 | ImGui::TableSetupColumn("Dashboard", ImGuiTableColumnFlags_WidthStretch); | ||
| 345 | ImGui::TableSetupColumn("Use", ImGuiTableColumnFlags_WidthFixed, 60); | ||
| 346 | ImGui::TableHeadersRow(); | ||
| 347 | for (size_t i = 0; i < pools.size(); i++) { | ||
| 348 | const BtfPoolAnnouncement& ann = pools[i]; | ||
| 349 | ImGui::TableNextRow(); | ||
| 350 | ImGui::TableSetColumnIndex(0); | ||
| 351 | ImGui::TextUnformatted(ann.poolName.c_str()); | ||
| 352 | ImGui::TextDisabled("%s", ann.btfAddress.c_str()); | ||
| 353 | ImGui::TableSetColumnIndex(1); | ||
| 354 | ImGui::Text("%.2f%%", ann.feePercent); | ||
| 355 | ImGui::TableSetColumnIndex(2); | ||
| 356 | ImGui::Text("%d miners, %d blocks, %s old", ann.connectedMiners, ann.blocksFound, FmtAge(ann.createdAt).c_str()); | ||
| 357 | ImGui::TableSetColumnIndex(3); | ||
| 358 | if (!ann.dashboardUrl.empty()) | ||
| 359 | ImGui::TextWrapped("%s", ann.dashboardUrl.c_str()); | ||
| 360 | else | ||
| 361 | ImGui::TextDisabled("-"); | ||
| 362 | ImGui::TableSetColumnIndex(4); | ||
| 363 | if (ImGui::SmallButton((std::string("Use##") + ann.btfAddress).c_str())) { | ||
| 364 | strncpy(g_participantPool, ann.btfAddress.c_str(), sizeof(g_participantPool)-1); | ||
| 365 | strParticipantPool = ann.btfAddress; | ||
| 366 | CWalletDB().WriteSetting("strParticipantPool", strParticipantPool); | ||
| 367 | g_needRefresh = true; | ||
| 368 | } | ||
| 369 | } | ||
| 370 | ImGui::EndTable(); | ||
| 371 | } | ||
| 372 | } | ||
| 373 | } | ||
| 374 | |||
| 375 | if (nMineMode == MINE_OPERATOR) { | ||
| 376 | ImGui::Spacing(); | ||
| 377 | ImGui::SeparatorText("Operator Overview"); | ||
| 378 | ImGui::Text("Pool .btf address:"); | ||
| 379 | std::string opAddr = BtfLocalAddress(); | ||
| 380 | if (opAddr.empty()) | ||
| 381 | ImGui::TextDisabled("Identity not ready yet"); | ||
| 382 | else | ||
| 383 | ImGui::TextWrapped("%s", opAddr.c_str()); | ||
| 384 | |||
| 385 | std::vector<PendingPayoutView> owed; | ||
| 386 | GetPendingPayouts(owed); | ||
| 387 | ImGui::Spacing(); | ||
| 388 | ImGui::SeparatorText("Pending Pool Payouts (you owe miners)"); | ||
| 389 | if (owed.empty()) { | ||
| 390 | ImGui::TextDisabled("No pending pool payouts"); | ||
| 391 | } else if (ImGui::BeginTable("owedpayouts", 4, | ||
| 392 | ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) | ||
| 393 | { | ||
| 394 | ImGui::TableSetupColumn("Matures At", ImGuiTableColumnFlags_WidthFixed, 100); | ||
| 395 | ImGui::TableSetupColumn("Blocks Left", ImGuiTableColumnFlags_WidthFixed, 90); | ||
| 396 | ImGui::TableSetupColumn("Recipients", ImGuiTableColumnFlags_WidthFixed, 90); | ||
| 397 | ImGui::TableSetupColumn("Total", ImGuiTableColumnFlags_WidthStretch); | ||
| 398 | ImGui::TableHeadersRow(); | ||
| 399 | for (const PendingPayoutView& row : owed) { | ||
| 400 | int remaining = std::max(0, row.matureAtHeight - nBestHeight); | ||
| 401 | ImGui::TableNextRow(); | ||
| 402 | ImGui::TableSetColumnIndex(0); | ||
| 403 | ImGui::Text("%d", row.matureAtHeight); | ||
| 404 | ImGui::TableSetColumnIndex(1); | ||
| 405 | ImGui::Text("%d", remaining); | ||
| 406 | ImGui::TableSetColumnIndex(2); | ||
| 407 | ImGui::Text("%d", row.recipients); | ||
| 408 | ImGui::TableSetColumnIndex(3); | ||
| 409 | ImGui::Text("%s BTF", FmtMoney(row.totalAmount).c_str()); | ||
| 410 | } | ||
| 411 | ImGui::EndTable(); | ||
| 412 | } | ||
| 413 | |||
| 414 | std::vector<PoolWorkerStatView> workers; | ||
| 415 | GetPoolWorkerStats(workers); | ||
| 416 | ImGui::Spacing(); | ||
| 417 | ImGui::SeparatorText("Current Round Workers"); | ||
| 418 | if (workers.empty()) { | ||
| 419 | ImGui::TextDisabled("No active workers in this round"); | ||
| 420 | } else if (ImGui::BeginTable("workers", 4, | ||
| 421 | ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) | ||
| 422 | { | ||
| 423 | ImGui::TableSetupColumn("Payout Addr", ImGuiTableColumnFlags_WidthStretch); | ||
| 424 | ImGui::TableSetupColumn("Worker", ImGuiTableColumnFlags_WidthStretch); | ||
| 425 | ImGui::TableSetupColumn("Round Shares", ImGuiTableColumnFlags_WidthFixed, 90); | ||
| 426 | ImGui::TableSetupColumn("Last Seen", ImGuiTableColumnFlags_WidthFixed, 100); | ||
| 427 | ImGui::TableHeadersRow(); | ||
| 428 | for (const PoolWorkerStatView& row : workers) { | ||
| 429 | ImGui::TableNextRow(); | ||
| 430 | ImGui::TableSetColumnIndex(0); | ||
| 431 | ImGui::TextUnformatted(row.address.empty() ? "unknown" : row.address.c_str()); | ||
| 432 | ImGui::TableSetColumnIndex(1); | ||
| 433 | ImGui::TextDisabled("%s", row.worker.empty() ? "worker" : row.worker.c_str()); | ||
| 434 | ImGui::TableSetColumnIndex(2); | ||
| 435 | ImGui::Text("%llu", (unsigned long long)row.roundShares); | ||
| 436 | ImGui::TableSetColumnIndex(3); | ||
| 437 | ImGui::TextDisabled("%s", FmtAge(row.lastSeen).c_str()); | ||
| 438 | } | ||
| 439 | ImGui::EndTable(); | ||
| 440 | } | ||
| 441 | } | ||
| 442 | |||
| 443 | // ---- Status bar ---- | ||
| 444 | { | ||
| 445 | ImGui::Spacing(); | ||
| 446 | ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4,2)); | ||
| 447 | |||
| 448 | int peers = (int)vNodes.size(); | ||
| 449 | if (nMineMode == MINE_PARTICIPANT) { | ||
| 450 | uint64 sent = 0, accepted = 0; | ||
| 451 | double hashRate = 0.0; | ||
| 452 | std::string mineStatus = GetParticipantMiningStatus(); | ||
| 453 | GetParticipantMiningStats(sent, accepted, hashRate); | ||
| 454 | if (sent > 0 || accepted > 0 || hashRate > 0.0) | ||
| 455 | ImGui::TextDisabled("Peers %d height %d shares sent %llu accepted %llu hash rate %.2f H/s", | ||
| 456 | peers, | ||
| 457 | nBestHeight, | ||
| 458 | (unsigned long long)sent, | ||
| 459 | (unsigned long long)accepted, | ||
| 460 | hashRate); | ||
| 461 | else if (!mineStatus.empty()) | ||
| 462 | ImGui::TextDisabled("Peers %d height %d %s", peers, nBestHeight, mineStatus.c_str()); | ||
| 463 | else | ||
| 464 | ImGui::TextDisabled("Peers %d height %d asking pool for work", peers, nBestHeight); | ||
| 465 | } else if (nMineMode == MINE_OPERATOR) { | ||
| 466 | int miners = 0, blocksFound = 0; | ||
| 467 | uint64 roundShares = 0; | ||
| 468 | GetPoolOperatorStats(miners, blocksFound, roundShares); | ||
| 469 | if (miners > 0 || blocksFound > 0 || roundShares > 0) | ||
| 470 | ImGui::TextDisabled("Peers %d height %d authorized miners %d round shares %llu blocks found %d", | ||
| 471 | peers, nBestHeight, miners, (unsigned long long)roundShares, blocksFound); | ||
| 472 | else | ||
| 473 | ImGui::TextDisabled("Peers %d height %d waiting for miners", peers, nBestHeight); | ||
| 474 | } else { | ||
| 475 | ImGui::TextDisabled("Peers %d height %d", peers, nBestHeight); | ||
| 476 | } | ||
| 477 | ImGui::PopStyleVar(); | ||
| 478 | } | ||
| 479 | |||
| 480 | ImGui::SetWindowFontScale(1.0f); | ||
| 481 | ImGui::End(); | ||
| 482 | } | ||
| 483 | |||
| 484 | // --------------------------------------------------------------------------- | ||
| 485 | // Send dialog | ||
| 486 | // --------------------------------------------------------------------------- | ||
| 487 | static void DrawSendDialog() | ||
| 488 | { | ||
| 489 | if (!g_showSend) return; | ||
| 490 | ImGui::SetNextWindowSize(ImVec2(500, 180), ImGuiCond_Always); | ||
| 491 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f)); | ||
| 492 | if (ImGui::Begin("Send Coins", &g_showSend, | ||
| 493 | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) | ||
| 494 | { | ||
| 495 | ImGui::Text("Recipient address:"); | ||
| 496 | ImGui::SetNextItemWidth(-1); | ||
| 497 | ImGui::InputText("##sa", g_sendAddr, sizeof(g_sendAddr)); | ||
| 498 | |||
| 499 | ImGui::Text("Amount (BTF):"); | ||
| 500 | ImGui::SetNextItemWidth(180); | ||
| 501 | ImGui::InputText("##sm", g_sendAmount, sizeof(g_sendAmount)); | ||
| 502 | |||
| 503 | if (!g_sendStatus.empty()) { | ||
| 504 | ImGui::Spacing(); | ||
| 505 | ImGui::TextColored(ImVec4(1,.4f,.4f,1), "%s", g_sendStatus.c_str()); | ||
| 506 | } | ||
| 507 | |||
| 508 | ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); | ||
| 509 | if (ImGui::Button("Send", ImVec2(90,0))) { | ||
| 510 | uint160 h; int64 nv = 0; | ||
| 511 | if (!AddressToHash160(g_sendAddr, h)) g_sendStatus = "Invalid address."; | ||
| 512 | else if (!ParseMoney(g_sendAmount,nv)||nv<=0) g_sendStatus = "Invalid amount."; | ||
| 513 | else { | ||
| 514 | CScript s; s << OP_DUP << OP_HASH160 << h << OP_EQUALVERIFY << OP_CHECKSIG; | ||
| 515 | CWalletTx wtx; | ||
| 516 | if (SendMoney(s, nv, wtx)) { | ||
| 517 | g_showSend=false; g_sendStatus=""; | ||
| 518 | memset(g_sendAddr,0,sizeof(g_sendAddr)); | ||
| 519 | memset(g_sendAmount,0,sizeof(g_sendAmount)); | ||
| 520 | g_needRefresh = true; | ||
| 521 | } else g_sendStatus = "Transaction failed."; | ||
| 522 | } | ||
| 523 | } | ||
| 524 | ImGui::SameLine(); | ||
| 525 | if (ImGui::Button("Cancel", ImVec2(90,0))) { g_showSend=false; g_sendStatus=""; } | ||
| 526 | } | ||
| 527 | ImGui::End(); | ||
| 528 | } | ||
| 529 | |||
| 530 | // --------------------------------------------------------------------------- | ||
| 531 | // Options dialog | ||
| 532 | // --------------------------------------------------------------------------- | ||
| 533 | static void DrawOptionsDialog() | ||
| 534 | { | ||
| 535 | if (!g_showOptions) return; | ||
| 536 | |||
| 537 | // Always sync from current globals when dialog opens | ||
| 538 | static bool wasOpen = false; | ||
| 539 | if (!wasOpen) { | ||
| 540 | g_mineRadio = nMineMode; | ||
| 541 | strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); | ||
| 542 | strncpy(g_poolName, strPoolName.c_str(), sizeof(g_poolName)-1); | ||
| 543 | snprintf(g_poolFee, sizeof(g_poolFee), "%.2f", dPoolFeePercent); | ||
| 544 | strncpy(g_poolDash, strPoolDashboardUrl.c_str(), sizeof(g_poolDash)-1); | ||
| 545 | } | ||
| 546 | wasOpen = true; | ||
| 547 | ImGui::SetNextWindowSize(ImVec2(500, 310), ImGuiCond_Always); | ||
| 548 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f)); | ||
| 549 | if (ImGui::Begin("Options", &g_showOptions, | ||
| 550 | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) | ||
| 551 | { | ||
| 552 | // Transaction fee | ||
| 553 | ImGui::SeparatorText("Transaction Fee"); | ||
| 554 | static char feeStr[32] = {}; | ||
| 555 | if (feeStr[0]=='\0') snprintf(feeStr,sizeof(feeStr),"%s",FmtMoney(nTransactionFee).c_str()); | ||
| 556 | ImGui::SetNextItemWidth(180); | ||
| 557 | ImGui::InputText("BTF per transaction##fee", feeStr, sizeof(feeStr)); | ||
| 558 | |||
| 559 | // Mining | ||
| 560 | ImGui::SeparatorText("Mining Mode"); | ||
| 561 | ImGui::RadioButton("Solo — rewards go to your wallet", &g_mineRadio, MINE_SOLO); | ||
| 562 | ImGui::RadioButton("Operator — run a pool for other miners", &g_mineRadio, MINE_OPERATOR); | ||
| 563 | ImGui::RadioButton("Participant — mine to someone's pool", &g_mineRadio, MINE_PARTICIPANT); | ||
| 564 | |||
| 565 | if (g_mineRadio == MINE_OPERATOR) { | ||
| 566 | ImGui::Spacing(); | ||
| 567 | ImGui::SeparatorText("Pool Announcement"); | ||
| 568 | ImGui::Text("Pool name:"); | ||
| 569 | ImGui::SetNextItemWidth(-1); | ||
| 570 | ImGui::InputText("##poolname", g_poolName, sizeof(g_poolName)); | ||
| 571 | ImGui::Text("Fee %%:"); | ||
| 572 | ImGui::SetNextItemWidth(120); | ||
| 573 | ImGui::InputText("##poolfee", g_poolFee, sizeof(g_poolFee)); | ||
| 574 | ImGui::Text("Dashboard URL:"); | ||
| 575 | ImGui::SetNextItemWidth(-1); | ||
| 576 | ImGui::InputText("##pooldash", g_poolDash, sizeof(g_poolDash)); | ||
| 577 | } | ||
| 578 | |||
| 579 | if (g_mineRadio == MINE_PARTICIPANT) { | ||
| 580 | ImGui::Spacing(); | ||
| 581 | ImGui::SeparatorText("Pool Selection"); | ||
| 582 | ImGui::TextDisabled("Pick from live discovered pools. Manual entry is disabled here."); | ||
| 583 | |||
| 584 | std::vector<BtfPoolAnnouncement> pools; | ||
| 585 | BtfGetPoolAnnouncements(pools); | ||
| 586 | if (!pools.empty()) { | ||
| 587 | std::string current = "Select a live pool"; | ||
| 588 | for (size_t i = 0; i < pools.size(); i++) { | ||
| 589 | if (pools[i].btfAddress == g_participantPool) { | ||
| 590 | current = PoolLabel(pools[i]); | ||
| 591 | break; | ||
| 592 | } | ||
| 593 | } | ||
| 594 | if (ImGui::BeginCombo("Discovered pools", current.c_str())) { | ||
| 595 | for (size_t i = 0; i < pools.size(); i++) { | ||
| 596 | const BtfPoolAnnouncement& ann = pools[i]; | ||
| 597 | std::string label = PoolLabel(ann); | ||
| 598 | bool selected = ann.btfAddress == g_participantPool; | ||
| 599 | if (ImGui::Selectable(label.c_str(), selected)) { | ||
| 600 | strncpy(g_participantPool, ann.btfAddress.c_str(), sizeof(g_participantPool)-1); | ||
| 601 | } | ||
| 602 | } | ||
| 603 | ImGui::EndCombo(); | ||
| 604 | } | ||
| 605 | } else { | ||
| 606 | ImGui::TextDisabled("No live pools discovered yet"); | ||
| 607 | } | ||
| 608 | } | ||
| 609 | |||
| 610 | ImGui::Spacing(); | ||
| 611 | const char* genLabel = fGenerateBitcoins ? "Stop Mining" : "Start Mining"; | ||
| 612 | if (ImGui::Button(genLabel, ImVec2(130,0))) { | ||
| 613 | fGenerateBitcoins = fGenerateBitcoins ? 0 : 1; | ||
| 614 | if (fGenerateBitcoins) _beginthread(ThreadBitcoinMiner, 0, NULL); | ||
| 615 | CWalletDB().WriteSetting("fGenerateBitcoins", fGenerateBitcoins); | ||
| 616 | } | ||
| 617 | |||
| 618 | ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); | ||
| 619 | if (ImGui::Button("OK", ImVec2(90,0))) { | ||
| 620 | int64 fee=0; | ||
| 621 | if (ParseMoney(feeStr,fee)) { nTransactionFee=fee; CWalletDB().WriteSetting("nTransactionFee",nTransactionFee); } | ||
| 622 | |||
| 623 | bool changed = (g_mineRadio != nMineMode); | ||
| 624 | nMineMode = g_mineRadio; | ||
| 625 | strParticipantPool = g_participantPool; | ||
| 626 | strPoolName = g_poolName[0] ? g_poolName : "Bitflash Pool"; | ||
| 627 | strPoolDashboardUrl = g_poolDash; | ||
| 628 | dPoolFeePercent = atof(g_poolFee); | ||
| 629 | CWalletDB().WriteSetting("nMineMode", nMineMode); | ||
| 630 | CWalletDB().WriteSetting("strParticipantPool", strParticipantPool); | ||
| 631 | CWalletDB().WriteSetting("strPoolName", strPoolName); | ||
| 632 | CWalletDB().WriteSetting("strPoolDashboardUrl", strPoolDashboardUrl); | ||
| 633 | CWalletDB().WriteSetting("dPoolFeePercent", dPoolFeePercent); | ||
| 634 | |||
| 635 | if (nMineMode == MINE_OPERATOR) { | ||
| 636 | if (!gPoolServerRunning) { | ||
| 637 | gPoolServerRunning = true; | ||
| 638 | _beginthread(ThreadRPCServer, 0, NULL); | ||
| 639 | } else if (!gPoolRunning) { | ||
| 640 | gPoolRunning = true; | ||
| 641 | } | ||
| 642 | } else { | ||
| 643 | gPoolRunning = false; | ||
| 644 | } | ||
| 645 | g_showOptions = false; | ||
| 646 | } | ||
| 647 | ImGui::SameLine(); | ||
| 648 | if (ImGui::Button("Cancel", ImVec2(90,0))) g_showOptions = false; | ||
| 649 | } | ||
| 650 | if (!g_showOptions) wasOpen = false; | ||
| 651 | ImGui::End(); | ||
| 652 | } | ||
| 653 | |||
| 654 | // --------------------------------------------------------------------------- | ||
| 655 | // About dialog | ||
| 656 | // --------------------------------------------------------------------------- | ||
| 657 | static void DrawAboutDialog() | ||
| 658 | { | ||
| 659 | if (!g_showAbout) return; | ||
| 660 | ImGui::SetNextWindowSize(ImVec2(380, 170), ImGuiCond_Always); | ||
| 661 | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(.5f,.5f)); | ||
| 662 | if (ImGui::Begin("About Bitflash", &g_showAbout, | ||
| 663 | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) | ||
| 664 | { | ||
| 665 | ImGui::TextColored(ImVec4(1,.6f,0,1), "Bitflash BTF v1.1.0"); | ||
| 666 | ImGui::Spacing(); | ||
| 667 | ImGui::TextWrapped("CPU-only cryptocurrency. RandomX proof of work. Anonymous .btf addressing over Nostr. No premine."); | ||
| 668 | ImGui::Spacing(); | ||
| 669 | ImGui::TextDisabled("Based on Bitcoin 0.1.0 (Satoshi Nakamoto, 2009)"); | ||
| 670 | ImGui::Spacing(); | ||
| 671 | if (ImGui::Button("OK", ImVec2(90,0))) g_showAbout = false; | ||
| 672 | } | ||
| 673 | ImGui::End(); | ||
| 674 | } | ||
| 675 | |||
| 676 | // --------------------------------------------------------------------------- | ||
| 677 | // Entry point | ||
| 678 | // --------------------------------------------------------------------------- | ||
| 679 | int RunGUI(int argc, char* argv[]) | ||
| 680 | { | ||
| 681 | if (!glfwInit()) return 1; | ||
| 682 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); | ||
| 683 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); | ||
| 684 | glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); | ||
| 685 | #ifdef __APPLE__ | ||
| 686 | glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); | ||
| 687 | #endif | ||
| 688 | |||
| 689 | GLFWwindow* window = glfwCreateWindow(960, 640, "Bitflash", nullptr, nullptr); | ||
| 690 | if (!window) { glfwTerminate(); return 1; } | ||
| 691 | glfwMakeContextCurrent(window); | ||
| 692 | glfwSwapInterval(1); | ||
| 693 | |||
| 694 | IMGUI_CHECKVERSION(); | ||
| 695 | ImGui::CreateContext(); | ||
| 696 | ImGuiIO& io = ImGui::GetIO(); | ||
| 697 | io.IniFilename = nullptr; | ||
| 698 | |||
| 699 | // Load Roboto at 16px — clean, readable on all platforms | ||
| 700 | io.Fonts->AddFontFromMemoryCompressedTTF( | ||
| 701 | RobotoMedium_compressed_data, RobotoMedium_compressed_size, 16.0f); | ||
| 702 | |||
| 703 | ImGui::StyleColorsDark(); | ||
| 704 | ImGuiStyle& s = ImGui::GetStyle(); | ||
| 705 | s.WindowRounding = s.FrameRounding = s.PopupRounding = 4.0f; | ||
| 706 | s.ItemSpacing = ImVec2(10, 7); | ||
| 707 | s.FramePadding = ImVec2(8, 5); | ||
| 708 | s.WindowPadding = ImVec2(12, 12); | ||
| 709 | // Orange accent | ||
| 710 | s.Colors[ImGuiCol_TitleBgActive] = ImVec4(.80f,.45f,.00f,1); | ||
| 711 | s.Colors[ImGuiCol_CheckMark] = ImVec4(1.0f,.60f,.00f,1); | ||
| 712 | s.Colors[ImGuiCol_SliderGrab] = ImVec4(.80f,.45f,.00f,1); | ||
| 713 | s.Colors[ImGuiCol_Button] = ImVec4(.25f,.25f,.25f,1); | ||
| 714 | s.Colors[ImGuiCol_ButtonHovered] = ImVec4(.80f,.45f,.00f,1); | ||
| 715 | s.Colors[ImGuiCol_ButtonActive] = ImVec4(.60f,.30f,.00f,1); | ||
| 716 | s.Colors[ImGuiCol_Header] = ImVec4(.80f,.45f,.00f,.40f); | ||
| 717 | s.Colors[ImGuiCol_HeaderHovered] = ImVec4(.80f,.45f,.20f,.80f); | ||
| 718 | s.Colors[ImGuiCol_Tab] = ImVec4(.20f,.20f,.20f,1); | ||
| 719 | s.Colors[ImGuiCol_TabHovered] = ImVec4(.80f,.45f,.00f,1); | ||
| 720 | s.Colors[ImGuiCol_TabActive] = ImVec4(.80f,.45f,.00f,1); | ||
| 721 | s.Colors[ImGuiCol_SeparatorHovered]=ImVec4(.80f,.45f,.00f,1); | ||
| 722 | s.Colors[ImGuiCol_FrameBg] = ImVec4(.14f,.14f,.14f,1); | ||
| 723 | s.Colors[ImGuiCol_FrameBgHovered] = ImVec4(.20f,.20f,.20f,1); | ||
| 724 | s.Colors[ImGuiCol_TableHeaderBg] = ImVec4(.18f,.18f,.18f,1); | ||
| 725 | s.Colors[ImGuiCol_TableRowBg] = ImVec4(.11f,.11f,.11f,1); | ||
| 726 | s.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(.14f,.14f,.14f,1); | ||
| 727 | |||
| 728 | ImGui_ImplGlfw_InitForOpenGL(window, true); | ||
| 729 | ImGui_ImplOpenGL3_Init("#version 330"); | ||
| 730 | |||
| 731 | g_mineRadio = nMineMode; | ||
| 732 | strncpy(g_participantPool, strParticipantPool.c_str(), sizeof(g_participantPool)-1); | ||
| 733 | |||
| 734 | int frame = 0; | ||
| 735 | while (!glfwWindowShouldClose(window) && !fShutdown) | ||
| 736 | { | ||
| 737 | glfwPollEvents(); | ||
| 738 | if (g_needRefresh || (frame % 120 == 0)) { RefreshWallet(); g_needRefresh = false; } | ||
| 739 | frame++; | ||
| 740 | |||
| 741 | ImGui_ImplOpenGL3_NewFrame(); | ||
| 742 | ImGui_ImplGlfw_NewFrame(); | ||
| 743 | ImGui::NewFrame(); | ||
| 744 | |||
| 745 | DrawMainWindow(); | ||
| 746 | DrawSendDialog(); | ||
| 747 | DrawOptionsDialog(); | ||
| 748 | DrawAboutDialog(); | ||
| 749 | |||
| 750 | ImGui::Render(); | ||
| 751 | int w, h; glfwGetFramebufferSize(window, &w, &h); | ||
| 752 | glViewport(0, 0, w, h); | ||
| 753 | glClearColor(.08f,.08f,.08f,1); | ||
| 754 | glClear(GL_COLOR_BUFFER_BIT); | ||
| 755 | ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); | ||
| 756 | glfwSwapBuffers(window); | ||
| 757 | } | ||
| 758 | |||
| 759 | fShutdown = true; | ||
| 760 | ImGui_ImplOpenGL3_Shutdown(); | ||
| 761 | ImGui_ImplGlfw_Shutdown(); | ||
| 762 | ImGui::DestroyContext(); | ||
| 763 | glfwDestroyWindow(window); | ||
| 764 | glfwTerminate(); | ||
| 765 | return 0; | ||
| 766 | } | ||