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*)&params.nodeps); igSameLine();
617 	igCheckbox("force", cast(bool*)&params.force); igSameLine();
618 	igCheckbox("x64", cast(bool*)&params.arch64); igSameLine();
619 	//igCheckbox("release", cast(bool*)&params.release); igSameLine();
620 
621 	withWidth!(60, igCombo2)("##buildType", cast(int*)&params.buildType, buildTypeUiSelectionString.ptr, 3); igSameLine();
622 	withWidth!(40, igCombo2)("##compiler", cast(int*)&params.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 }