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 playMenu.init(&launcher); 93 codeMenu.init(&launcher); 94 refresh(); 95 96 window = new GlfwWindow(); 97 window.init(uvec2(810, 600), "Voxelman launcher"); 98 igState.init(window.handle); 99 window.keyPressed.connect(&igState.onKeyPressed); 100 window.keyReleased.connect(&igState.onKeyReleased); 101 window.charEntered.connect(&igState.charCallback); 102 window.mousePressed.connect(&igState.onMousePressed); 103 window.mouseReleased.connect(&igState.onMouseReleased); 104 window.wheelScrolled.connect((dvec2 s) => igState.scrollCallback(s.y)); 105 106 selectedMenu = SelectedMenu.play; 107 playMenu.selectedMenu = PlayMenu.SelectedMenu.connect; 108 109 if (window is null) 110 isRunning = false; 111 112 setStyle(); 113 } 114 115 void run() 116 { 117 DerelictGL3.load(); 118 DerelictGLFW3.load(getLibName("", "glfw3")); 119 DerelictImgui.load(getLibName("", "cimgui")); 120 121 init(); 122 123 while(isRunning) 124 { 125 if (glfwWindowShouldClose(window.handle) && !launcher.anyProcessesRunning) 126 isRunning = false; 127 else 128 glfwSetWindowShouldClose(window.handle, false); 129 update(); 130 render(); 131 } 132 133 close(); 134 } 135 136 void refresh() 137 { 138 launcher.clear(); 139 launcher.setRootPath(pluginFolder, pluginPackFolder, toolFolder); 140 launcher.readPlugins(); 141 launcher.readPluginPacks(); 142 launcher.readServers(); 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 newGame, 269 connect, 270 load, 271 } 272 Launcher* launcher; 273 SelectedMenu selectedMenu; 274 ItemList!(PluginPack*) pluginPacks; 275 ItemList!(ServerInfo*) servers; 276 AddServerDialog addServerDlg; 277 278 void init(Launcher* launcher) 279 { 280 this.launcher = launcher; 281 addServerDlg.launcher = launcher; 282 } 283 284 void refresh() 285 { 286 pluginPacks.items = &launcher.pluginPacks; 287 servers.items = &launcher.servers; 288 } 289 290 void draw() 291 { 292 pluginPacks.update(); 293 igBeginGroup(); 294 295 if (igButton("New")) 296 selectedMenu = SelectedMenu.newGame; 297 igSameLine(); 298 if (igButton("Connect")) 299 selectedMenu = SelectedMenu.connect; 300 igSameLine(); 301 if (igButton("Load")) 302 selectedMenu = SelectedMenu.load; 303 304 if (selectedMenu == SelectedMenu.newGame) 305 drawNewGame(); 306 else if (selectedMenu == SelectedMenu.connect) 307 drawConnect(); 308 309 igEndGroup(); 310 } 311 312 void drawNewGame() 313 { 314 string pluginpack = "default"; 315 if (auto pack = pluginPacks.selected) 316 pluginpack = pack.id; 317 318 // ------------------------ PACKAGES ----------------------------------- 319 igBeginChild("packs", ImVec2(100, -igGetItemsLineHeightWithSpacing()), true); 320 foreach(int i, pluginPack; *pluginPacks.items) 321 { 322 igPushIdInt(cast(int)i); 323 immutable bool itemSelected = (i == pluginPacks.currentItem); 324 325 if (igSelectable(pluginPack.id.ptr, itemSelected)) 326 pluginPacks.currentItem = i; 327 328 igPopId(); 329 } 330 igEndChild(); 331 332 igSameLine(); 333 334 // ------------------------ PLUGINS ------------------------------------ 335 if (pluginPacks.hasSelected) 336 { 337 igBeginChild("pack's plugins", ImVec2(220, -igGetItemsLineHeightWithSpacing()), true); 338 foreach(int i, plugin; pluginPacks.selected.plugins) 339 { 340 igPushIdInt(cast(int)i); 341 igTextUnformatted(plugin.id.ptr, plugin.id.ptr+plugin.id.length); 342 igPopId(); 343 } 344 igEndChild(); 345 } 346 347 // ------------------------ BUTTONS ------------------------------------ 348 igBeginGroup(); 349 startButtons(launcher, pluginpack); 350 igSameLine(); 351 if (igButton("Stop")) 352 { 353 size_t numKilled = launcher.stopProcesses(); 354 launcher.appLog.put(format("killed %s processes\n", numKilled)); 355 } 356 igEndGroup(); 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.items.length > 0) 379 { 380 igSameLine(); 381 if (igButton("Remove")) 382 launcher.removeServer(servers.currentItem); 383 igSameLine(); 384 if (igButton("Connect")) 385 connect(); 386 } 387 } 388 389 void connect() 390 { 391 392 } 393 394 void pluginPackPlugins() 395 { 396 397 } 398 } 399 400 struct AddServerDialog 401 { 402 char[128] serverInputBuffer; 403 char[16] ipAddress; 404 int port = DEFAULT_PORT; 405 Launcher* launcher; 406 407 bool show() 408 { 409 if (igButton("Add")) 410 igOpenPopup("add"); 411 if (igBeginPopupModal("add", null, ImGuiWindowFlags_AlwaysAutoResize)) 412 { 413 igInputText("Server name", serverInputBuffer.ptr, serverInputBuffer.length); 414 igInputText("IP/port", ipAddress.ptr, ipAddress.length, ImGuiInputTextFlags_CharsDecimal); 415 igSameLine(); 416 igInputInt("##port", &port); 417 port = clamp(port, 0, ushort.max); 418 419 if (igButton("Add")) 420 { 421 launcher.addServer(ServerInfo( 422 serverInputBuffer.fromCString(), 423 ipAddress.fromCString(), 424 cast(ushort)port)); 425 resetFields(); 426 427 igCloseCurrentPopup(); 428 return true; 429 } 430 igSameLine(); 431 if (igButton("Cancel")) 432 igCloseCurrentPopup(); 433 434 igEndPopup(); 435 } 436 return false; 437 } 438 439 void resetFields() 440 { 441 Launcher* l = launcher; 442 this = AddServerDialog(); 443 launcher = l; 444 } 445 } 446 447 import std.traits : Parameters; 448 auto withWidth(float width, alias func)(auto ref Parameters!func args) 449 { 450 scope(exit) igPopItemWidth(); 451 igPushItemWidth(width); 452 return func(args); 453 } 454 455 struct CodeMenu 456 { 457 Launcher* launcher; 458 ItemList!(PluginPack*) pluginPacks; 459 460 void init(Launcher* launcher) 461 { 462 this.launcher = launcher; 463 } 464 465 void refresh() 466 { 467 pluginPacks.items = &launcher.pluginPacks; 468 } 469 470 bool getItem(int idx, const(char)** out_text) 471 { 472 *out_text = (*pluginPacks.items)[idx].id.ptr; 473 return true; 474 } 475 476 void draw() 477 { 478 igBeginGroup(); 479 withWidth!(150, igCombo3)( 480 "Pack", 481 cast(int*)&pluginPacks.currentItem, 482 &getter, &this, 483 cast(int)pluginPacks.items.length, -1); 484 igSameLine(); 485 startButtons(launcher, pluginPacks.selected.id); 486 487 float areaHeight = igGetWindowHeight() - igGetCursorPosY() - 10; 488 489 enum minItemHeight = 160; 490 size_t numJobs = launcher.jobs.length; 491 float itemHeight = (numJobs) ? areaHeight / numJobs : minItemHeight; 492 if (itemHeight < minItemHeight) itemHeight = minItemHeight; 493 foreach(job; launcher.jobs) drawJobLog(job, itemHeight); 494 495 igEndGroup(); 496 } 497 498 static extern (C) 499 bool getter(void* codeMenu, int idx, const(char)** out_text) 500 { 501 auto cm = cast(CodeMenu*)codeMenu; 502 return cm.getItem(idx, out_text); 503 } 504 } 505 506 void startButtons(Launcher* launcher, string pack) 507 { 508 static JobParams params; 509 params.runParameters["pack"] = pack; 510 511 params.appType = AppType.client; 512 if (igButton("Client")) launcher.createJob(params); igSameLine(); 513 514 params.appType = AppType.server; 515 if (igButton("Server")) launcher.createJob(params); 516 } 517 518 void jobParams(JobParams* params) 519 { 520 igCheckbox("nodeps", cast(bool*)¶ms.nodeps); igSameLine(); 521 igCheckbox("force", cast(bool*)¶ms.force); igSameLine(); 522 igCheckbox("x64", cast(bool*)¶ms.arch64); igSameLine(); 523 igCheckbox("release", cast(bool*)¶ms.release); igSameLine(); 524 525 withWidth!(40, igCombo2)("##compiler", cast(int*)¶ms.compiler, "dmd\0ldc\0\0", 2); 526 } 527 528 void drawJobLog(J)(J* job, float height) 529 { 530 igPushIdPtr(job); 531 assert(job.title.ptr); 532 auto state = jobStateString(job); 533 auto textPtrs = makeFormattedTextPtrs("%s %s\0", state, job.title); 534 535 igBeginChildEx(igGetIdPtr(job), ImVec2(0, height), true, ImGuiWindowFlags_HorizontalScrollbar); 536 igTextUnformatted(textPtrs.start, textPtrs.end-1); 537 igSameLine(); 538 jobParams(&job.params); 539 igSameLine(); 540 drawActionButtons(job); 541 if (job.command) 542 { 543 igInputText("", cast(char*)job.command.ptr, job.command.length, ImGuiInputTextFlags_ReadOnly); 544 } 545 job.messageWindow.draw(); 546 igEndChild(); 547 548 igPopId(); 549 } 550 551 void drawActionButtons(J)(J* job) 552 { 553 if (igButton("Clear")) job.messageWindow.lineBuffer.clear(); 554 if (!job.isRunning && !job.needsRestart) { 555 igSameLine(); 556 if (igButton("Close")) job.needsClose = true; 557 igSameLine(); 558 559 int jobType = 4; 560 if (igButton(" Run ")) jobType = JobType.run; igSameLine(); 561 if (igButton("Test")) jobType = JobType.test; igSameLine(); 562 if (igButton("Build")) jobType = JobType.compile; igSameLine(); 563 if (igButton(" B&R ")) jobType = JobType.compileAndRun; 564 if (jobType != 4) 565 { 566 job.needsRestart = true; 567 job.params.jobType = cast(JobType)jobType; 568 } 569 } else { 570 igSameLine(); 571 if (igButton("Stop")) job.sendCommand("stop"); 572 } 573 } 574 575 void setStyle() 576 { 577 ImGuiStyle* style = igGetStyle(); 578 style.Colors[ImGuiCol_Text] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f); 579 style.Colors[ImGuiCol_WindowBg] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); 580 style.Colors[ImGuiCol_Border] = ImVec4(0.00f, 0.00f, 0.20f, 0.65f); 581 style.Colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.12f); 582 style.Colors[ImGuiCol_FrameBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.39f); 583 style.Colors[ImGuiCol_MenuBarBg] = ImVec4(1.00f, 1.00f, 1.00f, 0.80f); 584 style.Colors[ImGuiCol_ScrollbarBg] = ImVec4(0.47f, 0.47f, 0.47f, 0.00f); 585 style.Colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.55f, 0.55f, 0.55f, 1.00f); 586 style.Colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.55f, 0.55f, 0.55f, 1.00f); 587 style.Colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.55f, 0.55f, 0.55f, 1.00f); 588 style.Colors[ImGuiCol_ComboBg] = ImVec4(0.80f, 0.80f, 0.80f, 1.00f); 589 style.Colors[ImGuiCol_CheckMark] = ImVec4(0.36f, 0.40f, 0.71f, 0.60f); 590 style.Colors[ImGuiCol_SliderGrab] = ImVec4(0.52f, 0.56f, 1.00f, 0.60f); 591 style.Colors[ImGuiCol_SliderGrabActive] = ImVec4(0.36f, 0.40f, 0.71f, 0.60f); 592 style.Colors[ImGuiCol_Button] = ImVec4(0.52f, 0.56f, 1.00f, 0.60f); 593 style.Colors[ImGuiCol_ButtonHovered] = ImVec4(0.43f, 0.46f, 0.82f, 0.60f); 594 style.Colors[ImGuiCol_ButtonActive] = ImVec4(0.37f, 0.40f, 0.71f, 0.60f); 595 style.Colors[ImGuiCol_TooltipBg] = ImVec4(0.86f, 0.86f, 0.86f, 0.90f); 596 //style.Colors[ImGuiCol_ModalWindowDarkening] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); 597 style.WindowFillAlphaDefault = 1.0f; 598 } 599 600 enum mainWindowFlags = ImGuiWindowFlags_NoTitleBar | 601 ImGuiWindowFlags_NoResize | 602 ImGuiWindowFlags_NoMove | 603 ImGuiWindowFlags_NoCollapse | 604 ImGuiWindowFlags_NoSavedSettings; 605 //ImGuiWindowFlags_MenuBar;