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*)&params.nodeps); igSameLine();
521 	igCheckbox("force", cast(bool*)&params.force); igSameLine();
522 	igCheckbox("x64", cast(bool*)&params.arch64); igSameLine();
523 	igCheckbox("release", cast(bool*)&params.release); igSameLine();
524 
525 	withWidth!(40, igCombo2)("##compiler", cast(int*)&params.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;