1 module nanogui.sdlbackend;
2 
3 import std.algorithm: map;
4 import std.array: array;
5 import std.exception: enforce;
6 import std.file: thisExePath;
7 import std.path: dirName, buildPath;
8 import std.range: iota;
9 import std.datetime : Clock;
10 
11 import std.experimental.logger: Logger, NullLogger, FileLogger, globalLogLevel, LogLevel;
12 
13 import gfm.math: mat4f, vec3f, vec4f;
14 import gfm.opengl: OpenGL;
15 import gfm.sdl2: SDL2, SDL2Window, SDL_Event, SDL_Cursor, SDL_SetCursor, 
16 	SDL_FreeCursor, SDL_Delay;
17 
18 import arsd.nanovega : nvgCreateContext, kill, NVGContextFlag;
19 import nanogui.screen : Screen;
20 import nanogui.theme : Theme;
21 import nanogui.common : NanoContext, Vector2i, MouseButton, MouseAction, Cursor;
22 
23 class SdlBackend : Screen
24 {
25 	this(int w, int h, string title)
26 	{
27 		/* Avoid locale-related number parsing issues */
28 		version(Windows) {}
29 		else {
30 			import core.stdc.locale;
31 			setlocale(LC_NUMERIC, "C");
32 		}
33 
34 		import gfm.sdl2, gfm.opengl;
35 		import bindbc.sdl;
36 
37 		this.width = w;
38 		this.height = h;
39 
40 		// create a logger
41 		import std.stdio : stdout;
42 		_log = new FileLogger(stdout);
43 
44 		// load dynamic libraries
45 		SDLSupport ret = loadSDL();
46 		if(ret != sdlSupport) {
47 			if(ret == SDLSupport.noLibrary) {
48 				/*
49 				The system failed to load the library. Usually this means that either the library or one of its dependencies could not be found.
50 				*/
51 			}
52 			else if(SDLSupport.badLibrary) {
53 				/*
54 				This indicates that the system was able to find and successfully load the library, but one or more symbols the binding expected to find was missing. This usually indicates that the loaded library is of a lower API version than the binding was configured to load, e.g., an SDL 2.0.2 library loaded by an SDL 2.0.10 configuration.
55 
56 				For many C libraries, including SDL, this is perfectly fine and the application can continue as long as none of the missing functions are called.
57 				*/
58 			}
59 		}
60 		_sdl2 = new SDL2(_log);
61 		globalLogLevel = LogLevel.error;
62 
63 		// You have to initialize each SDL subsystem you want by hand
64 		_sdl2.subSystemInit(SDL_INIT_VIDEO);
65 		_sdl2.subSystemInit(SDL_INIT_EVENTS);
66 
67 		SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
68 		SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
69 		SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
70 		SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
71 
72 		// create an OpenGL-enabled SDL window
73 		window = new SDL2Window(_sdl2,
74 								SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
75 								width, height,
76 								SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN );
77 
78 		window.setTitle(title);
79 
80 		GLSupport retVal = loadOpenGL();
81 		if(retVal >= GLSupport.gl33)
82 		{
83 			// configure renderer for OpenGL 3.3
84 			import std.stdio;
85 			writefln("Available version of opengl: %s", retVal);
86 		}
87 		else
88 		{
89 			import std.stdio;
90 			if (retVal == GLSupport.noLibrary)
91 				writeln("opengl is not available");
92 			else
93 				writefln("Unsupported version of opengl %s", retVal);
94 			import std.exception;
95 			enforce(0);
96 		}
97 
98 		_gl = new OpenGL(_log);
99 
100 		// redirect OpenGL output to our Logger
101 		_gl.redirectDebugOutput();
102 
103 		ctx = NanoContext(NVGContextFlag.Debug);
104 		enforce(ctx !is null, "cannot initialize NanoGui");
105 
106 		mCursorSet[Cursor.Arrow]     = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW);
107 		mCursorSet[Cursor.IBeam]     = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_IBEAM);
108 		mCursorSet[Cursor.Crosshair] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_CROSSHAIR);
109 		mCursorSet[Cursor.Hand]      = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_HAND);
110 		mCursorSet[Cursor.HResize]   = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZEWE);
111 		mCursorSet[Cursor.VResize]   = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZENS);
112 
113 		super(width, height, Clock.currTime.stdTime);
114 		theme = new Theme(ctx);
115 	}
116 
117 	~this()
118 	{
119 		SDL_FreeCursor(mCursorSet[Cursor.Arrow]);
120 		SDL_FreeCursor(mCursorSet[Cursor.IBeam]);
121 		SDL_FreeCursor(mCursorSet[Cursor.Crosshair]);
122 		SDL_FreeCursor(mCursorSet[Cursor.Hand]);
123 		SDL_FreeCursor(mCursorSet[Cursor.HResize]);
124 		SDL_FreeCursor(mCursorSet[Cursor.VResize]);
125 
126 		ctx.kill();
127 		_gl.destroy();
128 		window.destroy();
129 		_sdl2.destroy();
130 	}
131 
132 	private void delegate () _onBeforeLoopStart;
133 	void onBeforeLoopStart(void delegate () dg)
134 	{
135 		_onBeforeLoopStart = dg;
136 	}
137 
138 	void run()
139 	{
140 		import gfm.sdl2;
141 
142 		window.hide;
143 		SDL_FlushEvents(SDL_WINDOWEVENT, SDL_SYSWMEVENT);
144 
145 		onVisibleForTheFirstTime();
146 		window.show;
147 
148 		SDL_Event event;
149 
150 		bool running = true;
151 		while (running)
152 		{
153 			if (_onBeforeLoopStart)
154 				_onBeforeLoopStart();
155 
156 			SDL_PumpEvents();
157 
158 			while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_SYSWMEVENT))
159 			{
160 				switch (event.type)
161 				{
162 					case SDL_WINDOWEVENT:
163 					{
164 						switch (event.window.event)
165 						{
166 							case SDL_WINDOWEVENT_MOVED:
167 								// window has been moved to other position
168 								break;
169 
170 							case SDL_WINDOWEVENT_RESIZED:
171 							case SDL_WINDOWEVENT_SIZE_CHANGED:
172 							{
173 								// window size has been resized
174 								with(event.window)
175 								{
176 									width = data1;
177 									height = data2;
178 									resizeEvent(size);
179 								}
180 								break;
181 							}
182 
183 							case SDL_WINDOWEVENT_SHOWN:
184 							case SDL_WINDOWEVENT_FOCUS_GAINED:
185 							case SDL_WINDOWEVENT_RESTORED:
186 							case SDL_WINDOWEVENT_MAXIMIZED:
187 								// window has been activated
188 								break;
189 
190 							case SDL_WINDOWEVENT_HIDDEN:
191 							case SDL_WINDOWEVENT_FOCUS_LOST:
192 							case SDL_WINDOWEVENT_MINIMIZED:
193 								// window has been deactivated
194 								break;
195 
196 							case SDL_WINDOWEVENT_ENTER:
197 								// mouse cursor has entered window
198 								// for example default cursor can be disable
199 								// using SDL_ShowCursor(SDL_FALSE);
200 								break;
201 
202 							case SDL_WINDOWEVENT_LEAVE:
203 								// mouse cursor has left window
204 								// for example default cursor can be disable
205 								// using SDL_ShowCursor(SDL_TRUE);
206 								break;
207 
208 							case SDL_WINDOWEVENT_CLOSE:
209 								running = false;
210 								break;
211 							default:
212 						}
213 						break;
214 					}
215 					default:
216 				}
217 			}
218 
219 			// mouse update
220 			{
221 				while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_MOUSEMOTION, SDL_MOUSEWHEEL))
222 				{
223 					switch (event.type)
224 					{
225 					case SDL_MOUSEBUTTONDOWN:
226 						onMouseDown(event);
227 						// force redrawing
228 						mNeedToDraw = true;
229 						break;
230 					case SDL_MOUSEBUTTONUP:
231 						onMouseUp(event);
232 						// force redrawing
233 						mNeedToDraw = true;
234 						break;
235 					case SDL_MOUSEMOTION:
236 						onMouseMotion(event);
237 						// force redrawing
238 						mNeedToDraw = true;
239 						break;
240 					case SDL_MOUSEWHEEL:
241 						onMouseWheel(event);
242 						// force redrawing
243 						mNeedToDraw = true;
244 						break;
245 					default:
246 					}
247 				}
248 			}
249 
250 			// keyboard update
251 			{
252 				while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_KEYDOWN, SDL_KEYUP))
253 				{
254 					switch (event.type)
255 					{
256 						case SDL_KEYDOWN:
257 							onKeyDown(event);
258 							// force redrawing
259 							mNeedToDraw = true;
260 							break;
261 						case SDL_KEYUP:
262 							onKeyUp(event);
263 							// force redrawing
264 							mNeedToDraw = true;
265 							break;
266 						default:
267 					}
268 				}
269 			}
270 
271 			// text update
272 			{
273 				while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_TEXTINPUT, SDL_TEXTINPUT))
274 				{
275 					switch (event.type)
276 					{
277 						case SDL_TEXTINPUT:
278 							import core.stdc.string : strlen;
279 							auto len = strlen(&event.text.text[0]);
280 							if (!len)
281 								break;
282 							assert(len < event.text.text.sizeof);
283 							auto txt = event.text.text[0..len];
284 							import std.utf : byDchar;
285 							foreach(ch; txt.byDchar)
286 								super.keyboardCharacterEvent(ch);
287 
288 							// force redrawing
289 							mNeedToDraw = true;
290 							break;
291 						default:
292 							break;
293 					}
294 				}
295 			}
296 
297 			// user event, we use it as timer notification
298 			{
299 				while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_USEREVENT, SDL_USEREVENT))
300 				{
301 					switch (event.type)
302 					{
303 						case SDL_USEREVENT:
304 							// force redrawing
305 							mNeedToDraw = true;
306 							break;
307 						default:
308 							break;
309 					}
310 				}
311 			}
312 
313 			// perform drawing if needed
314 			{
315 				import std.datetime : dur;
316 
317 				static auto pauseTimeMs = 0;
318 				currTime = Clock.currTime.stdTime;
319 				if (currTime - mBlinkingCursorTimestamp > dur!"msecs"(500).total!"hnsecs")
320 				{
321 					mBlinkingCursorVisible = !mBlinkingCursorVisible;
322 					mNeedToDraw = true;
323 					mBlinkingCursorTimestamp = currTime;
324 				}
325 
326 				if (needToDraw)
327 				{
328 					pauseTimeMs = 0;
329 					size = Vector2i(width, height);
330 					super.draw(ctx);
331 
332 					window.swapBuffers();
333 				}
334 				else
335 				{
336 					pauseTimeMs = pauseTimeMs * 2 + 1; // exponential pause
337 					if (pauseTimeMs > 100)
338 						pauseTimeMs = 100; // max 100ms of pause
339 					SDL_Delay(pauseTimeMs);
340 				}
341 			}
342 		}
343 	}
344 
345 	abstract void onVisibleForTheFirstTime();
346 
347 	override Logger logger() { return _log; }
348 
349 protected:
350 	SDL2Window window;
351 	int width;
352 	int height;
353 
354 	MouseButton btn;
355 	MouseAction action;
356 	int modifiers;
357 
358 	Logger _log;
359 	OpenGL _gl;
360 	SDL2 _sdl2;
361 
362 	NanoContext ctx;
363 
364 	SDL_Cursor*[6] mCursorSet;
365 
366 	public void onKeyDown(ref const(SDL_Event) event)
367 	{
368 		import nanogui.common : KeyAction;
369 
370 		auto key = event.key.keysym.sym.convertSdlKeyToNanoguiKey;
371 		int modifiers = event.key.keysym.mod.convertSdlModifierToNanoguiModifier;
372 		super.keyboardEvent(key, event.key.keysym.scancode, KeyAction.Press, modifiers);
373 	}
374 
375 	public void onKeyUp(ref const(SDL_Event) event)
376 	{
377 		
378 	}
379 
380 	public void onMouseWheel(ref const(SDL_Event) event)
381 	{
382 		if (event.wheel.y > 0)
383 		{
384 			btn = MouseButton.WheelUp;
385 			super.scrollCallbackEvent(0, +1, Clock.currTime.stdTime);
386 		}
387 		else if (event.wheel.y < 0)
388 		{
389 			btn = MouseButton.WheelDown;
390 			super.scrollCallbackEvent(0, -1, Clock.currTime.stdTime);
391 		}
392 	}
393 	
394 	public void onMouseMotion(ref const(SDL_Event) event)
395 	{
396 		import gfm.sdl2 : SDL_BUTTON_LMASK, SDL_BUTTON_RMASK, SDL_BUTTON_MMASK;
397 
398 		ctx.mouse.x = event.motion.x;
399 		ctx.mouse.y = event.motion.y;
400 
401 		if (event.motion.state & SDL_BUTTON_LMASK)
402 			btn = MouseButton.Left;
403 		else if (event.motion.state & SDL_BUTTON_RMASK)
404 			btn = MouseButton.Right;
405 		else if (event.motion.state & SDL_BUTTON_MMASK)
406 			btn = MouseButton.Middle;
407 
408 		if (event.motion.state & SDL_BUTTON_LMASK)
409 			modifiers |= MouseButton.Left;
410 		if (event.motion.state & SDL_BUTTON_RMASK)
411 			modifiers |= MouseButton.Right;
412 		if (event.motion.state & SDL_BUTTON_MMASK)
413 			modifiers |= MouseButton.Middle;
414 
415 		action = MouseAction.Motion;
416 		super.cursorPosCallbackEvent(ctx.mouse.x, ctx.mouse.y, Clock.currTime.stdTime);
417 	}
418 
419 	public void onMouseUp(ref const(SDL_Event) event)
420 	{
421 		import gfm.sdl2 : SDL_BUTTON_LEFT, SDL_BUTTON_RIGHT, SDL_BUTTON_MIDDLE;
422 
423 		switch(event.button.button)
424 		{
425 			case SDL_BUTTON_LEFT:
426 				btn = MouseButton.Left;
427 			break;
428 			case SDL_BUTTON_RIGHT:
429 				btn = MouseButton.Right;
430 			break;
431 			case SDL_BUTTON_MIDDLE:
432 				btn = MouseButton.Middle;
433 			break;
434 			default:
435 		}
436 		action = MouseAction.Release;
437 		super.mouseButtonCallbackEvent(btn, action, modifiers, Clock.currTime.stdTime);
438 	}
439 
440 	public void onMouseDown(ref const(SDL_Event) event)
441 	{
442 		import gfm.sdl2 : SDL_BUTTON_LEFT, SDL_BUTTON_RIGHT, SDL_BUTTON_MIDDLE;
443 
444 		switch(event.button.button)
445 		{
446 			case SDL_BUTTON_LEFT:
447 				btn = MouseButton.Left;
448 			break;
449 			case SDL_BUTTON_RIGHT:
450 				btn = MouseButton.Right;
451 			break;
452 			case SDL_BUTTON_MIDDLE:
453 				btn = MouseButton.Middle;
454 			break;
455 			default:
456 		}
457 		action = MouseAction.Press;
458 		super.mouseButtonCallbackEvent(btn, action, modifiers, Clock.currTime.stdTime);
459 	}
460 
461 	override void cursor(Cursor value)
462 	{
463 		mCursor = value;
464 		SDL_SetCursor(mCursorSet[mCursor]);
465 	}
466 
467 	override Cursor cursor() const
468 	{
469 		return mCursor;
470 	}
471 }
472 
473 private auto convertSdlKeyToNanoguiKey(int sdlkey)
474 {
475 	import gfm.sdl2;
476 	import nanogui.common : KeyAction, Key;
477 
478 	int nanogui_key;
479 	switch(sdlkey)
480 	{
481 		case SDLK_LEFT:
482 			nanogui_key = Key.Left;
483 		break;
484 		case SDLK_RIGHT:
485 			nanogui_key = Key.Right;
486 		break;
487 		case SDLK_UP:
488 			nanogui_key = Key.Up;
489 		break;
490 		case SDLK_DOWN:
491 			nanogui_key = Key.Down;
492 		break;
493 		case SDLK_BACKSPACE:
494 			nanogui_key = Key.Backspace;
495 		break;
496 		case SDLK_DELETE:
497 			nanogui_key = Key.Delete;
498 		break;
499 		case SDLK_HOME:
500 			nanogui_key = Key.Home;
501 		break;
502 		case SDLK_END:
503 			nanogui_key = Key.End;
504 		break;
505 		case SDLK_RETURN:
506 			nanogui_key = Key.Enter;
507 		break;
508 		case SDLK_a:
509 			nanogui_key = Key.A;
510 		break;
511 		case SDLK_x:
512 			nanogui_key = Key.X;
513 		break;
514 		case SDLK_c:
515 			nanogui_key = Key.C;
516 		break;
517 		case SDLK_v:
518 			nanogui_key = Key.V;
519 		break;
520 		case SDLK_ESCAPE:
521 			nanogui_key = Key.Esc;
522 		break;
523 		default:
524 			nanogui_key = sdlkey;
525 	}
526 
527 	return nanogui_key;
528 }
529 
530 private auto convertSdlModifierToNanoguiModifier(int mod)
531 {
532 	import gfm.sdl2;
533 	import nanogui.common : KeyMod;
534 
535 	int nanogui_mod;
536 
537 	if (mod & KMOD_LCTRL)
538 		nanogui_mod |= KeyMod.Ctrl;
539 	if (mod & KMOD_LSHIFT)
540 		nanogui_mod |= KeyMod.Shift;
541 	if (mod & KMOD_LALT)
542 		nanogui_mod |= KeyMod.Alt;
543 	if (mod & KMOD_RCTRL)
544 		nanogui_mod |= KeyMod.Ctrl;
545 	if (mod & KMOD_RSHIFT)
546 		nanogui_mod |= KeyMod.Shift;
547 	if (mod & KMOD_RALT)
548 		nanogui_mod |= KeyMod.Alt;
549 
550 	return nanogui_mod;
551 }