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