1 /** 2 Copyright: Copyright (c) 2015-2016 Andrey Penechko. 3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 7 module gui; 8 9 import std.algorithm; 10 import std.array; 11 import std.experimental.logger; 12 import std.format; 13 import std.process; 14 import std.range; 15 import std.stdio; 16 import std..string : format, fromStringz; 17 import std.typecons : Flag, Yes, No; 18 19 import voxelman.math; 20 import derelict.glfw3.glfw3; 21 import derelict.imgui.imgui; 22 import derelict.opengl3.gl3; 23 import anchovy.glfwwindow; 24 import voxelman.imgui_glfw; 25 import voxelman.utils.libloader; 26 import voxelman.utils.textformatter; 27 import voxelman.utils.linebuffer; 28 29 import launcher; 30 31 32 struct ItemList(T) 33 { 34 T[]* items; 35 size_t currentItem; 36 bool hasSelected() @property { 37 return currentItem < (*items).length; 38 } 39 T selected() @property { 40 if (currentItem < (*items).length) 41 return (*items)[currentItem]; 42 else if ((*items).length > 0) 43 return (*items)[$-1]; 44 else 45 return T.init; 46 } 47 48 void update() { 49 if (currentItem >= (*items).length) 50 currentItem = (*items).length-1; 51 if ((*items).length == 0) 52 currentItem = 0; 53 } 54 55 ref T opIndex(size_t i) { 56 return (*items)[i]; 57 } 58 } 59 60 struct LauncherGui 61 { 62 bool show_test_window = true; 63 bool show_another_window = false; 64 float[3] clear_color = [0.3f, 0.4f, 0.6f]; 65 bool isRunning = true; 66 ImguiState igState; 67 GlfwWindow window; 68 69 Launcher launcher; 70 71 string pluginFolder = `./plugins`; 72 string pluginPackFolder = `./pluginpacks`; 73 string toolFolder = `./tools`; 74 ItemList!(PluginInfo*) plugins; 75 76 void init() 77 { 78 import std.datetime : SysTime; 79 import std.concurrency : Tid; 80 class ConciseLogger : FileLogger { 81 this(File file, const LogLevel lv = LogLevel.info) @safe { 82 super(file, lv); 83 } 84 85 override protected void beginLogMsg(string file, int line, string funcName, 86 string prettyFuncName, string moduleName, LogLevel logLevel, 87 Tid threadId, SysTime timestamp, Logger logger) 88 @safe {} 89 } 90 //auto file = File(filename, "w"); 91 auto logger = new MultiLogger; 92 //logger.insertLogger("fileLogger", new FileLogger(file)); 93 logger.insertLogger("stdoutLogger", new ConciseLogger(stdout)); 94 sharedLog = logger; 95 96 launcher.init(); 97 98 playMenu.init(&launcher); 99 codeMenu.init(&launcher); 100 refresh(); 101 102 window = new GlfwWindow(); 103 window.init(ivec2(820, 600), "Voxelman launcher"); 104 igState.init(window.handle); 105 window.keyPressed.connect(&igState.onKeyPressed); 106 window.keyReleased.connect(&igState.onKeyReleased); 107 window.charEntered.connect(&igState.charCallback); 108 window.mousePressed.connect(&igState.onMousePressed); 109 window.mouseReleased.connect(&igState.onMouseReleased); 110 window.wheelScrolled.connect((dvec2 s) => igState.scrollCallback(s.y)); 111 112 if (window is null) 113 isRunning = false; 114 115 setStyle(); 116 } 117 118 void run() 119 { 120 DerelictGL3.load(); 121 loadLib(DerelictGLFW3, "", "glfw3"); 122 loadLib(DerelictImgui, "", "cimgui"); 123 124 init(); 125 126 while(isRunning) 127 { 128 if (glfwWindowShouldClose(window.handle) && !launcher.anyProcessesRunning) 129 isRunning = false; 130 else 131 glfwSetWindowShouldClose(window.handle, false); 132 update(); 133 render(); 134 } 135 136 close(); 137 } 138 139 void refresh() 140 { 141 launcher.setRootPath(pluginFolder, pluginPackFolder, toolFolder); 142 launcher.refresh(); 143 plugins.items = &launcher.plugins; 144 playMenu.refresh(); 145 codeMenu.refresh(); 146 } 147 148 void update() 149 { 150 launcher.update(); 151 window.processEvents(); 152 igState.newFrame(); 153 doGui(); 154 155 import core.thread; 156 Thread.sleep(15.msecs); 157 } 158 159 void render() 160 { 161 ImGuiIO* io = igGetIO(); 162 glViewport(0, 0, cast(int)io.DisplaySize.x, cast(int)io.DisplaySize.y); 163 glClearColor(clear_color[0], clear_color[1], clear_color[2], 0); 164 glClear(GL_COLOR_BUFFER_BIT); 165 igState.render(); 166 glfwSwapBuffers(window.handle); 167 } 168 169 void close() 170 { 171 window.releaseWindow; 172 igState.shutdown(); 173 glfwTerminate(); 174 } 175 176 void doGui() 177 { 178 //igPushStyleVar(ImGuiStyleVar_FrameRounding, 0f); 179 //igGetStyle().FrameRounding = 0.0f; 180 igSetNextWindowPos(ImVec2(0,0)); 181 igSetNextWindowSize(igGetIO().DisplaySize); 182 if (igBegin("Main", null, mainWindowFlags)) 183 { 184 drawMainMenu(); 185 igSameLine(); 186 drawMenuContent(); 187 188 igEnd(); 189 } 190 //igPopStyleVar(); 191 //igShowTestWindow(null); 192 } 193 194 enum SelectedMenu 195 { 196 play, 197 code, 198 conf 199 } 200 201 SelectedMenu selectedMenu; 202 PlayMenu playMenu; 203 CodeMenu codeMenu; 204 205 void drawMainMenu() 206 { 207 igBeginGroup(); 208 209 menuEntry("Play", SelectedMenu.play); 210 menuEntry("Code", SelectedMenu.code); 211 menuEntry("Conf", SelectedMenu.conf); 212 213 //if (igButton("Refresh")) 214 // refresh(); 215 igSpacing(); 216 if (igButton("Exit")) 217 isRunning = false; 218 igEndGroup(); 219 } 220 221 void menuEntry(string text, SelectedMenu select) 222 { 223 ImGuiStyle* style = igGetStyle(); 224 const ImVec4 color = style.Colors[ImGuiCol_Button]; 225 const ImVec4 colorActive = style.Colors[ImGuiCol_ButtonActive]; 226 const ImVec4 colorHover = style.Colors[ImGuiCol_ButtonHovered]; 227 228 if (selectedMenu == select) 229 { 230 style.Colors[ImGuiCol_Button] = colorActive; 231 style.Colors[ImGuiCol_ButtonActive] = colorActive; 232 style.Colors[ImGuiCol_ButtonHovered] = colorActive; 233 } 234 else 235 { 236 style.Colors[ImGuiCol_Button] = color; 237 style.Colors[ImGuiCol_ButtonActive] = colorActive; 238 style.Colors[ImGuiCol_ButtonHovered] = colorHover; 239 } 240 241 if (igButton(text.ptr)) selectedMenu = select; 242 243 style.Colors[ImGuiCol_Button] = color; 244 style.Colors[ImGuiCol_ButtonActive] = colorActive; 245 style.Colors[ImGuiCol_ButtonHovered] = colorHover; 246 } 247 248 void drawMenuContent() 249 { 250 final switch(selectedMenu) with(SelectedMenu) 251 { 252 case play: 253 playMenu.draw(); 254 break; 255 case code: 256 codeMenu.draw(); 257 break; 258 case conf: 259 break; 260 } 261 } 262 } 263 264 struct PlayMenu 265 { 266 enum SelectedMenu 267 { 268 worlds, 269 connect, 270 newGame, 271 } 272 Launcher* launcher; 273 SelectedMenu selectedMenu; 274 ItemList!(PluginPack*) pluginPacks; 275 ItemList!(ServerInfo*) servers; 276 ItemList!(SaveInfo*) saves; 277 AddServerDialog addServerDlg; 278 NewSaveDialog newSaveDlg; 279 280 void init(Launcher* launcher) 281 { 282 this.launcher = launcher; 283 addServerDlg.launcher = launcher; 284 newSaveDlg.launcher = launcher; 285 } 286 287 void refresh() 288 { 289 pluginPacks.items = &launcher.pluginPacks; 290 servers.items = &launcher.servers; 291 saves.items = &launcher.saves; 292 } 293 294 void draw() 295 { 296 pluginPacks.update(); 297 servers.update(); 298 saves.update(); 299 igBeginGroup(); 300 301 if (igButton("Worlds##Play")) 302 selectedMenu = SelectedMenu.worlds; 303 igSameLine(); 304 if (igButton("Connect##Play")) 305 selectedMenu = SelectedMenu.connect; 306 //if (igButton("New##Play")) 307 // selectedMenu = SelectedMenu.newGame; 308 //igSameLine(); 309 310 final switch(selectedMenu) 311 { 312 case SelectedMenu.worlds: 313 drawWorlds(); break; 314 case SelectedMenu.connect: 315 drawConnect(); break; 316 case SelectedMenu.newGame: 317 drawNewGame(); break; 318 } 319 320 igEndGroup(); 321 } 322 323 void drawNewGame() 324 { 325 string pluginpack = "default"; 326 if (auto pack = pluginPacks.selected) 327 pluginpack = pack.id; 328 329 // ------------------------ PACKAGES ----------------------------------- 330 igBeginChild("packs", ImVec2(100, -igGetItemsLineHeightWithSpacing()), true); 331 foreach(int i, pluginPack; *pluginPacks.items) 332 { 333 igPushIdInt(cast(int)i); 334 immutable bool itemSelected = (i == pluginPacks.currentItem); 335 336 if (igSelectable(pluginPack.id.ptr, itemSelected)) 337 pluginPacks.currentItem = i; 338 339 igPopId(); 340 } 341 igEndChild(); 342 343 igSameLine(); 344 345 // ------------------------ PLUGINS ------------------------------------ 346 if (pluginPacks.hasSelected) 347 { 348 igBeginChild("pack's plugins", ImVec2(220, -igGetItemsLineHeightWithSpacing()), true); 349 foreach(int i, plugin; pluginPacks.selected.plugins) 350 { 351 igPushIdInt(cast(int)i); 352 igTextUnformatted(plugin.id.ptr, plugin.id.ptr+plugin.id.length); 353 igPopId(); 354 } 355 igEndChild(); 356 } 357 } 358 359 void drawConnect() 360 { 361 igBeginChild("Servers", ImVec2(400, -igGetItemsLineHeightWithSpacing()), true); 362 foreach(int i, server; *servers.items) 363 { 364 igPushIdInt(cast(int)i); 365 immutable bool itemSelected = (i == servers.currentItem); 366 367 if (igSelectable(server.name.ptr, itemSelected)) 368 servers.currentItem = i; 369 igPopId(); 370 } 371 igEndChild(); 372 373 if (addServerDlg.show()) 374 { 375 refresh(); 376 } 377 378 if (servers.hasSelected) 379 { 380 igSameLine(); 381 if (igButton("Remove##Servers")) 382 launcher.removeServer(servers.currentItem); 383 igSameLine(); 384 if (igButton("Connect")) 385 { 386 launcher.connect(servers.selected, pluginPacks.selected); 387 } 388 } 389 } 390 391 void drawWorlds() 392 { 393 enum tableWidth = 300; 394 igBeginChild("Saves", ImVec2(tableWidth, -igGetItemsLineHeightWithSpacing()), true); 395 igColumns(2); 396 igSetColumnOffset(1, tableWidth - 90); 397 foreach(int i, save; *saves.items) 398 { 399 igPushIdInt(cast(int)i); 400 immutable bool itemSelected = (i == saves.currentItem); 401 402 if (igSelectable(save.name.ptr, itemSelected, ImGuiSelectableFlags_SpanAllColumns)) 403 saves.currentItem = i; 404 igNextColumn(); 405 igTextUnformatted(save.displaySize.ptr, save.displaySize.ptr+save.displaySize.length); 406 igNextColumn(); 407 igPopId(); 408 } 409 igColumns(1); 410 igEndChild(); 411 412 size_t newSaveIndex; 413 if (newSaveDlg.show(newSaveIndex)) { 414 refresh(); 415 saves.currentItem = newSaveIndex; 416 } 417 igSameLine(); 418 419 if (saves.hasSelected) 420 { 421 if (igButton("Delete##Saves")) 422 igOpenPopup("Confirm"); 423 if (igBeginPopupModal("Confirm", null, ImGuiWindowFlags_AlwaysAutoResize)) 424 { 425 if (igButton("Delete##Confirm")) 426 { 427 launcher.deleteSave(saves.currentItem); 428 refresh(); 429 igCloseCurrentPopup(); 430 } 431 igSameLine(); 432 if (igButton("Cancel##Confirm")) 433 igCloseCurrentPopup(); 434 igEndPopup(); 435 } 436 } 437 438 igSameLine(); 439 if (igButton("Refresh##Saves")) { 440 launcher.refreshSaves(); 441 refresh(); 442 } 443 444 if (saves.hasSelected) 445 { 446 igSameLine(); 447 igSetCursorPosX(tableWidth - 50); 448 if (igButton("Server##Saves")) 449 { 450 launcher.startServer(pluginPacks.selected, saves.selected); 451 } 452 igSameLine(); 453 igSetCursorPosX(tableWidth + 10); 454 if (igButton("Start##Saves")) 455 { 456 launcher.startCombined(pluginPacks.selected, saves.selected); 457 } 458 } 459 } 460 } 461 462 struct NewSaveDialog 463 { 464 char[128] saveInputBuffer; 465 Launcher* launcher; 466 467 bool show(out size_t newSaveIndex) 468 { 469 bool result; 470 if (igButton("New")) 471 igOpenPopup("New world"); 472 if (igBeginPopupModal("New world", null, ImGuiWindowFlags_AlwaysAutoResize)) 473 { 474 bool entered; 475 if (igInputText("World name", saveInputBuffer.ptr, saveInputBuffer.length, ImGuiInputTextFlags_EnterReturnsTrue)) 476 { 477 entered = true; 478 } 479 480 if (igButton("Create") || entered) 481 { 482 newSaveIndex = launcher.createSave(saveInputBuffer.fromCString); 483 resetFields(); 484 485 igCloseCurrentPopup(); 486 result = true; 487 } 488 igSameLine(); 489 if (igButton("Cancel")) 490 igCloseCurrentPopup(); 491 492 igEndPopup(); 493 } 494 return result; 495 } 496 497 void resetFields() 498 { 499 saveInputBuffer[] = '\0'; 500 } 501 } 502 503 struct AddServerDialog 504 { 505 char[128] serverInputBuffer; 506 char[16] ipAddress; 507 int port = DEFAULT_PORT; 508 Launcher* launcher; 509 510 bool show() 511 { 512 if (igButton("Add")) 513 igOpenPopup("add"); 514 if (igBeginPopupModal("add", null, ImGuiWindowFlags_AlwaysAutoResize)) 515 { 516 igInputText("Server name", serverInputBuffer.ptr, serverInputBuffer.length); 517 igInputText("IP/port", ipAddress.ptr, ipAddress.length, ImGuiInputTextFlags_CharsDecimal); 518 igSameLine(); 519 igInputInt("##port", &port); 520 port = clamp(port, 0, ushort.max); 521 522 if (igButton("Add")) 523 { 524 launcher.addServer(ServerInfo( 525 serverInputBuffer.fromCString(), 526 ipAddress.fromCString(), 527 cast(ushort)port)); 528 resetFields(); 529 530 igCloseCurrentPopup(); 531 return true; 532 } 533 igSameLine(); 534 if (igButton("Cancel")) 535 igCloseCurrentPopup(); 536 537 igEndPopup(); 538 } 539 return false; 540 } 541 542 void resetFields() 543 { 544 Launcher* l = launcher; 545 this = AddServerDialog(); 546 launcher = l; 547 } 548 } 549 550 import std.traits : Parameters; 551 auto withWidth(float width, alias func)(auto ref Parameters!func args) 552 { 553 scope(exit) igPopItemWidth(); 554 igPushItemWidth(width); 555 return func(args); 556 } 557 558 extern (C) 559 bool itemGetter(T)(void* itemList, int idx, const(char)** out_text) 560 { 561 auto il = cast(T*)itemList; 562 *out_text = (*il.items)[idx].guiName.ptr; 563 return true; 564 } 565 566 struct CodeMenu 567 { 568 Launcher* launcher; 569 ItemList!(PluginPack*) pluginPacks; 570 ItemList!(SaveInfo*) saves; 571 572 void init(Launcher* launcher) 573 { 574 this.launcher = launcher; 575 } 576 577 void refresh() 578 { 579 pluginPacks.items = &launcher.pluginPacks; 580 saves.items = &launcher.saves; 581 } 582 583 void draw() 584 { 585 igBeginGroup(); 586 withWidth!(150, igCombo3)( 587 "Pack", 588 cast(int*)&pluginPacks.currentItem, 589 &itemGetter!(ItemList!(PluginPack*)), &pluginPacks, 590 cast(int)pluginPacks.items.length, -1); 591 igSameLine(); 592 withWidth!(150, igCombo3)( 593 "Save", 594 cast(int*)&saves.currentItem, 595 &itemGetter!(ItemList!(SaveInfo*)), &saves, 596 cast(int)saves.items.length, -1); 597 igSameLine(); 598 startButtons(launcher, pluginPacks.selected, saves.selected); 599 600 float areaHeight = igGetWindowHeight() - igGetCursorPosY() - 10; 601 602 enum minItemHeight = 160; 603 size_t numJobs = launcher.jobs.length; 604 float itemHeight = (numJobs) ? areaHeight / numJobs : minItemHeight; 605 if (itemHeight < minItemHeight) itemHeight = minItemHeight; 606 foreach(job; launcher.jobs) drawJobLog(job, itemHeight); 607 608 igEndGroup(); 609 } 610 } 611 612 void startButtons(Launcher* launcher, PluginPack* pack, SaveInfo* save) 613 { 614 static JobParams params; 615 if (pack) params.runParameters["pack"] = pack.id; 616 if (save) params.runParameters["world_name"] = save.name; 617 618 params.appType = AppType.client; 619 if (igButton("Client")) launcher.createJob(params); igSameLine(); 620 621 params.appType = AppType.server; 622 if (igButton("Server")) launcher.createJob(params); igSameLine(); 623 624 params.appType = AppType.combined; 625 if (igButton("Combined")) launcher.createJob(params); 626 } 627 628 void jobParams(JobParams* params) 629 { 630 igCheckbox("nodeps", cast(bool*)¶ms.nodeps); igSameLine(); 631 igCheckbox("force", cast(bool*)¶ms.force); igSameLine(); 632 igCheckbox("x64", cast(bool*)¶ms.arch64); igSameLine(); 633 //igCheckbox("release", cast(bool*)¶ms.release); igSameLine(); 634 635 withWidth!(60, igCombo2)("##buildType", cast(int*)¶ms.buildType, buildTypeUiSelectionString.ptr, 4); igSameLine(); 636 withWidth!(40, igCombo2)("##compiler", cast(int*)¶ms.compiler, compilerUiSelectionString.ptr, 2); 637 } 638 639 void drawJobLog(J)(J* job, float height) 640 { 641 igPushIdPtr(job); 642 assert(job.title.ptr); 643 auto state = jobStateString(job); 644 auto textPtrs = makeFormattedTextPtrs("%s %s\0", state, job.title); 645 646 igBeginChildEx(igGetIdPtr(job), ImVec2(0, height), true, ImGuiWindowFlags_HorizontalScrollbar); 647 igTextUnformatted(textPtrs.start, textPtrs.end-1); 648 igSameLine(); 649 jobParams(&job.params); 650 igSameLine(); 651 drawActionButtons(job); 652 if (job.command) 653 { 654 igPushItemWidth(-1); 655 igInputText("", cast(char*)job.command.ptr, job.command.length, ImGuiInputTextFlags_ReadOnly); 656 igPopItemWidth(); 657 } 658 job.messageWindow.draw(); 659 igEndChild(); 660 661 igPopId(); 662 } 663 664 void drawActionButtons(J)(J* job) 665 { 666 if (igButton("Clear")) job.messageWindow.lineBuffer.clear(); 667 if (!job.isRunning && !job.needsRestart) { 668 igSameLine(); 669 if (igButton("Close")) job.needsClose = true; 670 igSameLine(); 671 672 int jobType = int.max; 673 if (igButton(" Run ")) jobType = JobType.run; igSameLine(); 674 if (igButton("Build")) jobType = JobType.compile; igSameLine(); 675 if (igButton(" B&R ")) jobType = JobType.compileAndRun; 676 if (jobType != int.max) 677 { 678 job.needsRestart = true; 679 job.params.jobType = cast(JobType)jobType; 680 } 681 } else { 682 igSameLine(); 683 if (igButton("Stop")) job.sendCommand("stop"); 684 } 685 } 686 687 void setStyle() 688 { 689 ImGuiStyle* style = igGetStyle(); 690 style.Colors[ImGuiCol_Text] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f); 691 style.Colors[ImGuiCol_WindowBg] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); 692 style.Colors[ImGuiCol_Border] = ImVec4(0.00f, 0.00f, 0.20f, 0.65f); 693 style.Colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.12f); 694 style.Colors[ImGuiCol_FrameBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.39f); 695 style.Colors[ImGuiCol_MenuBarBg] = ImVec4(1.00f, 1.00f, 1.00f, 0.80f); 696 style.Colors[ImGuiCol_ScrollbarBg] = ImVec4(0.47f, 0.47f, 0.47f, 0.00f); 697 style.Colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.55f, 0.55f, 0.55f, 1.00f); 698 style.Colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.55f, 0.55f, 0.55f, 1.00f); 699 style.Colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.55f, 0.55f, 0.55f, 1.00f); 700 style.Colors[ImGuiCol_ComboBg] = ImVec4(0.80f, 0.80f, 0.80f, 1.00f); 701 style.Colors[ImGuiCol_CheckMark] = ImVec4(0.36f, 0.40f, 0.71f, 0.60f); 702 style.Colors[ImGuiCol_SliderGrab] = ImVec4(0.52f, 0.56f, 1.00f, 0.60f); 703 style.Colors[ImGuiCol_SliderGrabActive] = ImVec4(0.36f, 0.40f, 0.71f, 0.60f); 704 style.Colors[ImGuiCol_Button] = ImVec4(0.52f, 0.56f, 1.00f, 0.60f); 705 style.Colors[ImGuiCol_ButtonHovered] = ImVec4(0.43f, 0.46f, 0.82f, 0.60f); 706 style.Colors[ImGuiCol_ButtonActive] = ImVec4(0.37f, 0.40f, 0.71f, 0.60f); 707 style.Colors[ImGuiCol_TooltipBg] = ImVec4(0.86f, 0.86f, 0.86f, 0.90f); 708 //style.Colors[ImGuiCol_ModalWindowDarkening] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); 709 style.WindowFillAlphaDefault = 1.0f; 710 } 711 712 enum mainWindowFlags = ImGuiWindowFlags_NoTitleBar | 713 ImGuiWindowFlags_NoResize | 714 ImGuiWindowFlags_NoMove | 715 ImGuiWindowFlags_NoCollapse | 716 ImGuiWindowFlags_NoSavedSettings; 717 //ImGuiWindowFlags_MenuBar;