1 module evael.graphics.gui.GuiManager;
2 
3 import std.algorithm;
4 import std.array : array;
5 import std.file : read;
6 import std.json : parseJSON, JSONValue;
7 import std.conv : to;
8 import std.string : format;
9 import jsonizer;
10 
11 import evael.graphics.GUI;
12 import evael.graphics.GraphicsDevice;
13 
14 import evael.system.AssetLoader;
15 import evael.system.Input;
16 
17 import evael.utils.Math;
18 import evael.utils.Rectangle;
19 
20 /**
21  * GuiManager.
22  */
23 class GuiManager
24 {
25 	private GraphicsDevice m_graphicsDevice;
26 	private NVGcontext* m_nvg;
27 
28 	/// Container list
29 	private Container[] m_containers;
30 	
31 	/// Control that have the focus
32 	private Container m_focusedControl;
33 
34 	/// Control under the mouse
35 	private Container m_controlUnderMouse;
36 
37 	/// Drag'n'dropped control
38 	private Control m_dragAndDropControl;
39 
40 	private vec2 m_mousePosition;
41 	
42 	/// Default theme
43 	private Theme m_defaultTheme;
44 
45 	/// Current theme
46 	private Theme m_currentTheme;
47 
48 	private string[] m_customControlsTypes = ["button", "textBlock", "textBox", "progressBar", "textArea", "slider", 
49 			"panel", "listBox", "listBoxItem", "checkBox", "contextMenuStrip",
50 			"contextMenuStripItem", "pictureBox", "tooltip", "comboBox", "scrollBar"];
51 	
52 	/**
53 	 * GuiManager constructor.
54 	 */
55 	public this(GraphicsDevice graphics)
56 	{
57 		this.m_graphicsDevice = graphics;
58         this.m_nvg = graphics.nvgContext;
59 
60 		this.m_defaultTheme = this.loadTheme("medias\\ui\\themes\\default.json");
61 		this.m_currentTheme = this.m_defaultTheme;
62 	}
63 	
64 	/**
65 	 * GuiManager destructor.
66 	 */
67 	@nogc @safe
68 	public void dispose() pure nothrow
69 	{
70 
71 	}
72 
73 	public void update(in float deltaTime)
74 	{
75 		nvgBeginFrame(this.m_nvg, this.m_graphicsDevice.viewportSize.width, this.m_graphicsDevice.viewportSize.height, 1);
76 		
77 		foreach (control; this.m_containers)
78 		{
79 			control.draw(deltaTime);
80 		}
81 		
82 		nvgEndFrame(this.m_nvg);
83 	}
84 
85 	public void fixedUpdate(in float deltaTime)
86 	{
87 		foreach (control; this.m_containers)
88 		{
89 			control.update(deltaTime);
90 		}
91 	}
92 
93 
94 	/**
95 	 * Adds a container and initializes it.
96 	 * Params:
97 	 *		 container : container to add
98 	 */
99 	public void add(Container container)
100 	{
101 		container.nvg = this.m_nvg;
102 
103 		if (container.name in this.m_currentTheme.subThemes)
104 		{
105 			// This container have a custom theme
106 			container.theme = this.m_currentTheme.subThemes[container.name];
107 		}
108 		else
109 		{
110 			container.theme = this.m_currentTheme.copy();
111 		}
112 
113 		container.initialize();
114 
115 		this.m_containers ~= container;
116 	} 
117 
118 	/**
119 	 * Removes a control.
120 	 * Params:
121 	 *		 control : control to remove
122 	 */
123 	public void remove(Control toRemove)
124 	{
125 		foreach (i, control; this.m_containers)
126 		{
127 			if (control == toRemove)
128 			{
129 				this.m_containers = this.m_containers.remove(i);
130 				return;
131 			}
132 		}
133 	}
134 
135 
136 	/**
137 	 * Loads a theme.
138 	 * Params:
139 	 *		fileName : theme to load
140 	 */
141 	public Theme loadTheme(in string fileName)
142 	{
143 		// We load base theme
144 		auto themeJson = fileName.read().to!string().parseJSON();
145 		auto theme = themeJson.fromJSON!(Theme);
146 
147 		auto assetLoader = AssetLoader.getInstance();
148 		
149 		if (theme.font !is null)
150 		{
151 			theme.font = assetLoader.load!(Font)(theme.font.name, this.m_nvg);
152 			theme.iconFont = assetLoader.load!(Font)(theme.iconFont.name, this.m_nvg);			
153 		}
154 
155 		theme.parent = null;
156 		theme.name = "base";
157 		theme.subThemes = loadSubThemes(theme, themeJson);
158 
159 		return theme;
160 	}
161 
162 	private Theme[string] loadSubThemes(Theme parentTheme, JSONValue json)
163 	{
164 		Theme[string] toRet;
165 
166 		// We load controls themes
167 		foreach (controlType; this.m_customControlsTypes)
168 		{
169 			if (controlType in json)
170 			{
171 				auto controlJson = json[controlType];
172 
173 				// Theme has been found for this control, inherited from base theme
174 				auto controlTheme = parentTheme.copy();
175 				controlTheme.parent = parentTheme;
176 				controlTheme.name = parentTheme.name ~ "." ~ controlType;
177 
178 				// We need to update custom fields values
179 				foreach (i, dummy ; typeof(Theme.tupleof))
180 				{
181 					enum name = Theme.tupleof[i].stringof;
182 					enum type = typeof(Theme.tupleof[i]).stringof;
183 
184 					static if (type != "Theme*")
185 					{
186 						if(name in controlJson)
187 						{
188 							static if(type == "BorderType")
189 							{
190 								mixin(`controlTheme.%s = controlJson["%s"].fromJSON!(Theme.%s);`.format(name, name, type));
191 							}
192 							else
193 							{
194 								mixin(`controlTheme.%s = controlJson["%s"].fromJSON!(%s);`.format(name, name, type));
195 							}
196 						}
197 					}
198 				}
199 
200 				toRet[controlType] = controlTheme;
201 
202 				controlTheme.subThemes = loadSubThemes(controlTheme, controlJson);
203 			}
204 			
205 		}
206 
207 		return toRet;
208 	}
209 
210 	
211 	/**
212 	 * Sets current theme.
213 	 * Params:
214 	 *		theme : current theme for next controls
215 	 */
216 	@nogc @safe
217 	public void setTheme()(in auto ref Theme theme) pure nothrow
218 	{
219 		this.m_currentTheme = theme;
220 	}
221 
222 	/**
223 	 * Loads and sets current theme.
224 	 * Params:
225 	 *		themeName : theme to load
226 	 */
227 	public void setTheme(in string themeName)
228 	{
229 		this.m_currentTheme = this.loadTheme(themeName);
230 	}
231 
232 	/**
233 	 * Adds a custom control type for theme loading.
234 	 * Params:
235 	 *		type : custom type
236 	 */
237 	public void addCustomControlType(in string type)
238 	{
239 		this.m_customControlsTypes ~= type;
240 	}
241 	
242 	/**
243 	 * Event called on mouse button click action.
244 	 * Params:
245 	 *		position : mouse position
246 	 *		mouseButton : clicked mouse button
247 	 */
248 	public void onMouseClick(in MouseButton mouseButton, in ref vec2 mousePosition)
249 	{
250 		if (this.m_controlUnderMouse !is null)
251 		{
252 			this.m_controlUnderMouse.onMouseClick(mouseButton, mousePosition);			
253 						
254 			if (this.m_dragAndDropControl is null || !this.m_dragAndDropControl.isClicked)
255 			{
256 				this.m_dragAndDropControl = this.getDeepestControlUnderMouse(this.m_mousePosition);
257 				
258 				if (this.m_dragAndDropControl !is null)
259 				{					
260 					if(this.m_dragAndDropControl.canBeDragged())
261 					{
262 						this.m_dragAndDropControl.positionBeforeDragAndDrop = this.m_dragAndDropControl.realPosition;
263 					}
264 					else
265 					{
266 						this.m_dragAndDropControl = null;
267 					}
268 				}
269 			}
270 		}
271 	}
272 
273 	/**
274 	 * Event called on mouse button release action.
275 	 * Params:
276 	 *		position : mouse position
277 	 *		mouseButton : released mouse button
278 	 */
279 	public void onMouseUp(in MouseButton mouseButton)
280 	{
281 		if (this.m_controlUnderMouse !is null)
282 		{
283 			// We stop drag and drop only if user released the button used to drag the control
284 			if (this.m_dragAndDropControl !is null 
285 				&& (this.m_dragAndDropControl.draggingMouseButton.isNull || mouseButton == this.m_dragAndDropControl.draggingMouseButton))
286 			{
287 				// We need to check if mouse has been moved while clicked
288 				if (this.m_dragAndDropControl.realPosition != this.m_dragAndDropControl.positionBeforeDragAndDrop)
289 				{
290 					this.m_dragAndDropControl.onDrop(this.m_mousePosition);
291 					this.m_dragAndDropControl = null;
292 				}
293 			}
294 
295 			this.m_controlUnderMouse.onMouseUp(mouseButton);
296 
297 			if (this.m_controlUnderMouse.isFocusable)
298 			{
299 				this.focus(this.m_controlUnderMouse);
300 			}
301 		}
302 	}
303 
304 	/**
305 	 * Event called on mouse movement action.
306 	 * Params:
307 	 *		position : mouse position
308 	 */
309 	public void onMouseMove(in ref vec2 position)
310 	{	
311 		scope (exit)
312 		{
313 			this.m_mousePosition = position;
314 		}
315 		
316 		// We draw the control that is under the mouse at the end
317 		if (this.m_controlUnderMouse !is null)
318 		{
319 			this.m_containers.sort!((a, b) => b == this.m_controlUnderMouse);
320 		}
321 
322 		/**
323 		 * Special case : drag and drop
324 		 */
325 		if (this.m_dragAndDropControl !is null && this.m_dragAndDropControl.isClicked && this.m_dragAndDropControl.movable)
326 		{
327 			// Yes. We handle this.
328 			immutable newPosition = vec2(position.x - this.m_dragAndDropControl.size.halfWidth, position.y - this.m_dragAndDropControl.size.halfHeight);
329 
330 			this.m_dragAndDropControl.position = newPosition;
331 			this.m_dragAndDropControl.realPosition = newPosition;
332 
333 			this.m_dragAndDropControl.onDrag(position);
334 			
335 			return;
336 		}
337 
338 		/**
339 		 * Normal case
340 		 */
341 		auto lastControlUnderMouse = this.m_controlUnderMouse;
342 
343 		this.m_controlUnderMouse = this.getControlUnderMouse(position);
344 
345 		if (this.m_controlUnderMouse !is null)
346 		{
347 			this.m_controlUnderMouse.onMouseMove(position);
348 
349 			Control tooltipControl = this.m_controlUnderMouse;
350 
351 			// We search for the control under the mouse and update the tooltip text
352 			auto deepestFocusedControl = this.getDeepestControlUnderMouse(position);
353 			
354 			// At this point we have the last-level container under the mouse, 
355 			// if his tooltiptext is set or if he doesn't have child control under mouse,
356 			// we will use his tooltiptext
357 			if (deepestFocusedControl.tooltipText !is null || deepestFocusedControl.controlUnderMouse is null)
358 			{
359 				tooltipControl = deepestFocusedControl;
360 			}
361 			else
362 			{
363 				tooltipControl = deepestFocusedControl.controlUnderMouse;
364 			}
365 
366 			this.m_controlUnderMouse.tooltip.realPosition = vec2(position.x + 13, position.y + 5);			
367 			this.m_controlUnderMouse.tooltip.text = tooltipControl.tooltipText;
368 			this.m_controlUnderMouse.tooltip.show();
369 		}
370 
371 		// We call onMouseLeave on last control under mouse if its different
372 		if (lastControlUnderMouse !is null && this.m_controlUnderMouse != lastControlUnderMouse)
373 		{
374 			lastControlUnderMouse.onMouseLeave();
375 		}
376 	}
377 
378 	/**
379 	 * Event called on character input.
380 	 * Params:
381 	 *		text : 
382 	 */
383 	public void onText(in int text)
384 	{
385 		// %, esc, del, ctrl + a
386 		if (text == 37 || text == 27 || text == 8 || text == 1)
387 			return;
388 
389 		if (this.m_focusedControl !is null)
390 		{
391 			this.m_focusedControl.onText(text);
392 		}
393 	}
394 
395 	/**
396 	 * Event called on key action.
397 	 * Params:
398 	 *		key : pressed key
399 	 */
400 	public void onKey(in int key)
401 	{
402 		if(this.m_focusedControl !is null)
403 		{
404 			this.m_focusedControl.onKey(key);
405 		}
406 	}
407 
408 	/**
409 	 * Returns the first-level container under the mouse.
410 	 */
411 	private Container getControlUnderMouse(in ref vec2 position)
412 	{
413 		foreach(container; this.m_containers)
414 		{
415 			if(!container.isVisible)
416 			{
417 				continue;
418 			}
419 
420 			immutable rect = Rectanglef(container.position.x, container.position.y, container.size);
421 
422 			if(rect.isIn(position))
423 			{
424 				return container;
425 			}
426 		}
427 
428 		return null;
429 	}
430 
431 	/**
432 	 * Returns the last-level container under the mouse.
433 	 */
434 	private Container getDeepestControlUnderMouse(in ref vec2 position)
435 	{
436 		auto control = this.getControlUnderMouse(position);
437 
438 		if (control is null)
439 		{
440 			return null;
441 		}
442 
443 		auto deepestFocusedControl = control;
444 
445 		while (control !is null)
446 		{
447 			deepestFocusedControl = control;
448 			control = cast(Container) control.controlUnderMouse;
449 		}
450 
451 		return deepestFocusedControl;
452 	}
453 
454 
455 	/**
456 	 * Returns a control by his id.
457 	 * Params:
458 	 *		 id : control id
459 	 */
460 	public Control getControlById(in uint id) nothrow	
461 	{
462 		auto range = this.m_containers.filter!(c => c.id == id).array;
463 
464 		return range.length ? range[0] : null;
465 	}
466 
467 	/**
468 	 * Removes all controls.
469 	 */
470 	@nogc @safe
471 	public void clear() pure nothrow
472 	{
473 		this.m_containers = null;
474 	}
475 
476 	/**
477 	 * Gives focus to a control.
478 	 */
479 	public void focus(Container control)
480 	{
481 		control.focus();
482 
483 		this.m_focusedControl = control;
484 	}
485 
486 	/**
487 	 * Removes focus from a control.
488 	 */
489 	public void unfocus(Container control)
490 	{
491 		control.unfocus();
492 
493 		this.m_focusedControl = null;
494 	}
495 
496 	@nogc @safe
497 	@property pure nothrow
498 	{
499 		public Container focusedControl()
500 		{
501 			return this.m_focusedControl;
502 		}
503 
504 		public Control controlUnderMouse()
505 		{
506 			return this.m_controlUnderMouse;
507 		}
508 	}
509 }