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*)&params.nodeps); igSameLine();
624 	igCheckbox("force", cast(bool*)&params.force); igSameLine();
625 	igCheckbox("x64", cast(bool*)&params.arch64); igSameLine();
626 	igCheckbox("release", cast(bool*)&params.release); igSameLine();
627 
628 	withWidth!(40, igCombo2)("##compiler", cast(int*)&params.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;