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