1 module gfm.sdl2.sdl;
2 
3 import core.stdc.stdlib;
4 
5 import std.conv,
6        std..string,
7        std.array;
8 
9 import derelict.sdl2.sdl,
10        derelict.sdl2.image,
11        derelict.util.exception;
12 
13 import std.experimental.logger;
14 
15 import gfm.core.text,
16        gfm.sdl2.renderer,
17        gfm.sdl2.window,
18        gfm.sdl2.keyboard,
19        gfm.sdl2.mouse;
20 
21 /// The one exception type thrown in this wrapper.
22 /// A failing SDL function should <b>always</b> throw a $(D SDL2Exception).
23 class SDL2Exception : Exception
24 {
25     public
26     {
27         @safe pure nothrow this(string message, string file =__FILE__, size_t line = __LINE__, Throwable next = null)
28         {
29             super(message, file, line, next);
30         }
31     }
32 }
33 
34 /// Owns both the loader, logging, keyboard state...
35 /// This object is passed around to other SDL wrapper objects
36 /// to ensure library loading.
37 final class SDL2
38 {
39     public
40     {
41         /// Load SDL2 library, redirect logging to our logger.
42         /// You can pass a null logger if you don't want logging.
43         /// Throws: $(D SDL2Exception) on error.
44         /// TODO: Custom SDL assertion handler.
45         this(Logger logger)
46         {
47             _logger = logger is null ? new NullLogger() : logger;
48             _SDLInitialized = false;
49             _SDL2LoggingRedirected = false;
50             try
51             {
52                 DerelictSDL2.load();
53             }
54             catch(DerelictException e)
55             {
56                 throw new SDL2Exception(e.msg);
57             }
58 
59             // enable all logging, and pipe it to our own logger object
60             {
61                 SDL_LogGetOutputFunction(_previousLogCallback, &_previousLogUserdata);
62                 SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE);
63                 SDL_LogSetOutputFunction(&loggingCallbackSDL, cast(void*)this);
64 
65                 SDL_SetAssertionHandler(&assertCallbackSDL, cast(void*)this);
66                 _SDL2LoggingRedirected = true;
67             }
68 
69             if (0 != SDL_Init(0))
70                 throwSDL2Exception("SDL_Init");
71 
72             _logger.infof("Platform: %s, %s CPU", getPlatform(), getCPUCount());
73             
74             subSystemInit(SDL_INIT_TIMER);
75             subSystemInit(SDL_INIT_VIDEO);
76             subSystemInit(SDL_INIT_JOYSTICK);
77             subSystemInit(SDL_INIT_AUDIO);
78             subSystemInit(SDL_INIT_HAPTIC);
79 
80             _logger.infof("Running using video driver: %s", sanitizeUTF8(SDL_GetCurrentVideoDriver(), _logger, "SDL_GetCurrentVideoDriver"));
81 
82             int numDisplays = SDL_GetNumVideoDisplays();
83             
84             _logger.infof("%s video display(s) detected.", numDisplays);
85 
86             _keyboard = new SDL2Keyboard(this);
87             _mouse = new SDL2Mouse(this);
88         }
89 
90         /// Releases the SDL library and all resources.
91         /// All resources should have been released at this point,
92         /// since you won't be able to call any SDL function afterwards.
93         void close()
94         {
95             // restore previously set logging function
96             if (_SDL2LoggingRedirected)
97             {
98                 SDL_LogSetOutputFunction(_previousLogCallback, _previousLogUserdata);
99                 _SDL2LoggingRedirected = false;
100 
101                 SDL_SetAssertionHandler(null, cast(void*)this);
102             }
103 
104             if (_SDLInitialized)
105             {
106                 SDL_Quit();
107                 _SDLInitialized = false;
108             }
109 
110             if (DerelictSDL2.isLoaded())
111                 DerelictSDL2.unload();
112         }
113 
114         ~this()
115         {
116             close();
117         }
118 
119         /// Returns: Available displays information.
120         /// Throws: $(D SDL2Exception) on error.
121         SDL2VideoDisplay[] getDisplays()
122         {
123             int numDisplays = SDL_GetNumVideoDisplays();
124 
125             SDL2VideoDisplay[] availableDisplays;
126             
127             for (int displayIndex = 0; displayIndex < numDisplays; ++displayIndex)
128             {
129                 SDL_Rect rect;
130                 int res = SDL_GetDisplayBounds(displayIndex, &rect);
131                 if (res != 0)
132                     throwSDL2Exception("SDL_GetDisplayBounds");
133 
134                 SDL2DisplayMode[] availableModes;
135 
136                 int numModes = SDL_GetNumDisplayModes(displayIndex);
137                 for(int modeIndex = 0; modeIndex < numModes; ++modeIndex)
138                 {
139                     SDL_DisplayMode mode;
140                     if (0 != SDL_GetDisplayMode(displayIndex, modeIndex, &mode))
141                         throwSDL2Exception("SDL_GetDisplayMode");
142 
143                     availableModes ~= new SDL2DisplayMode(modeIndex, mode);
144                 }
145 
146                 availableDisplays ~= new SDL2VideoDisplay(displayIndex, rect, availableModes);
147             }
148             return availableDisplays;
149         }
150 
151         /// Returns: Resolution of the first display.
152         /// Throws: $(D SDL2Exception) on error.
153         SDL_Point firstDisplaySize()
154         {
155             auto displays = getDisplays();
156             if (displays.length == 0)
157                 throw new SDL2Exception("no display");
158             return displays[0].dimension();
159         }
160 
161         /// Returns: Available renderers information.
162         /// Throws: $(D SDL2Exception) on error.
163         SDL2RendererInfo[] getRenderersInfo()
164         {
165             SDL2RendererInfo[] res;
166             int num = SDL_GetNumRenderDrivers();
167             if (num < 0)
168                 throwSDL2Exception("SDL_GetNumRenderDrivers");
169 
170             for (int i = 0; i < num; ++i)
171             {
172                 SDL_RendererInfo info;
173                 int err = SDL_GetRenderDriverInfo(i, &info);
174                 if (err != 0)
175                     throwSDL2Exception("SDL_GetRenderDriverInfo");
176                 res ~= new SDL2RendererInfo(_logger, i, info);
177             }
178             return res;
179         }
180 
181         /// Get next SDL event.
182         /// Input state gets updated and window callbacks are called too.
183         /// Returns: true if returned an event.
184         bool pollEvent(SDL_Event* event)
185         {
186             if (SDL_PollEvent(event) != 0)
187             {
188                 updateState(event);
189                 dispatchEvent(event);
190                 return true;
191             }
192             else
193                 return false;
194         }   
195 
196         /// Process all pending SDL events.
197         /// Input state gets updated and window callbacks are called too.
198         void processEvents()
199         {
200             SDL_Event event;
201 
202             while(SDL_PollEvent(&event) != 0)
203             {
204                 updateState(&event);
205                 dispatchEvent(&event);
206             }
207         }
208 
209         /// Returns: Keyboard state.
210         /// The keyboard state is updated by processEvents() and pollEvent().
211         SDL2Keyboard keyboard()
212         {
213             return _keyboard;
214         }
215 
216         /// Returns: Mouse state.
217         /// The mouse state is updated by processEvents() and pollEvent().
218         SDL2Mouse mouse()
219         {
220             return _mouse;
221         }
222 
223         /// Returns: true if an application termination has been requested.
224         bool wasQuitRequested() const
225         {
226             return _quitWasRequested;
227         }        
228 
229         deprecated alias wasQuitResquested = wasQuitRequested;
230 
231         /// Start text input.
232         void startTextInput()
233         {
234             SDL_StartTextInput();
235         }
236 
237         /// Stops text input.
238         void stopTextInput()
239         {
240             SDL_StopTextInput();
241         }
242 
243         /// Sets clipboard content.
244         /// Throws: $(D SDL2Exception) on error.
245         string setClipboard(string s)
246         {
247             int err = SDL_SetClipboardText(toStringz(s));
248             if (err != 0)
249                 throwSDL2Exception("SDL_SetClipboardText");
250             return s;
251         }
252 
253         /// Returns: Clipboard content.
254         /// Throws: $(D SDL2Exception) on error.
255         string getClipboard()
256         {
257             if (SDL_HasClipboardText() == SDL_FALSE)
258                 return null;
259 
260             const(char)* s = SDL_GetClipboardText();
261             if (s is null)
262                 throwSDL2Exception("SDL_GetClipboardText");
263 
264             return sanitizeUTF8(s, _logger, "SDL clipboard text");
265         }   
266 
267         /// Returns: Available SDL video drivers.
268         string[] getVideoDrivers()
269         {
270             const int numDrivers = SDL_GetNumVideoDrivers();
271             string[] res;
272             res.length = numDrivers;
273             for(int i = 0; i < numDrivers; ++i)
274                 res[i] = sanitizeUTF8(SDL_GetVideoDriver(i), _logger, "SDL_GetVideoDriver");
275             return res;
276         }
277 
278         /// Returns: Platform name.
279         string getPlatform()
280         {
281             return sanitizeUTF8(SDL_GetPlatform(), _logger, "SDL_GetPlatform");
282         }
283 
284         /// Returns: L1 cacheline size in bytes.
285         int getL1LineSize()
286         {
287             int res = SDL_GetCPUCacheLineSize();
288             if (res <= 0)
289                 res = 64;
290             return res;
291         }
292 
293         /// Returns: number of CPUs.
294         int getCPUCount()
295         {
296             int res = SDL_GetCPUCount();
297             if (res <= 0)
298                 res = 1;
299             return res;
300         }
301 
302         /// Returns: A path suitable for writing configuration files, saved games, etc...
303         /// See_also: $(LINK http://wiki.libsdl.org/SDL_GetPrefPath)
304         /// Throws: $(D SDL2Exception) on error.
305         string getPrefPath(string orgName, string applicationName)
306         {
307             char* basePath = SDL_GetPrefPath(toStringz(orgName), toStringz(applicationName));
308             if (basePath != null) 
309             {
310                 string result = sanitizeUTF8(basePath, _logger, "SDL pref path");
311                 SDL_free(basePath);
312                 return result;
313             } 
314             else
315             {
316                 throwSDL2Exception("SDL_GetPrefPath");
317                 return null; // unreachable
318             }
319         }
320     }
321 
322     package
323     {
324         Logger _logger;
325 
326         // exception mechanism that shall be used by every module here
327         void throwSDL2Exception(string callThatFailed)
328         {
329             string message = format("%s failed: %s", callThatFailed, getErrorString());
330             throw new SDL2Exception(message);
331         }
332 
333         // return last SDL error and clears it
334         string getErrorString()
335         {
336             const(char)* message = SDL_GetError();
337             SDL_ClearError(); // clear error
338             return sanitizeUTF8(message, _logger, "SDL error string");
339         }
340 
341         void registerWindow(SDL2Window window)
342         {
343             _knownWindows[window.id()] = window;
344         }
345 
346         void unregisterWindow(SDL2Window window)
347         {
348             assert((window.id() in _knownWindows) !is null);
349             _knownWindows.remove(window.id());
350         }        
351     }
352 
353     private
354     {
355         bool _SDL2LoggingRedirected;
356         SDL_LogOutputFunction _previousLogCallback;
357         void* _previousLogUserdata;
358 
359 
360         bool _SDLInitialized;
361 
362         // all created windows are keeped in this map
363         // to be able to dispatch event
364         SDL2Window[uint] _knownWindows;
365 
366         // SDL_QUIT was received
367         bool _quitWasRequested = false;
368 
369         // Holds keyboard state
370         SDL2Keyboard _keyboard;
371 
372         // Holds mouse state
373         SDL2Mouse _mouse;
374 
375         bool subSystemInitialized(int subSystem)
376         {
377             int inited = SDL_WasInit(SDL_INIT_EVERYTHING);
378             return 0 != ( inited & subSystem );
379         }
380 
381         void subSystemInit(int flag)
382         {
383             if (!subSystemInitialized(flag))
384             {
385                 int res = SDL_InitSubSystem(flag);
386                 if (0 != res)
387                     throwSDL2Exception("SDL_InitSubSystem");
388             }
389         }
390 
391         void onLogMessage(int category, SDL_LogPriority priority, const(char)* message)
392         {
393             static string readablePriority(SDL_LogPriority priority) pure
394             {
395                 switch(priority)
396                 {
397                     case SDL_LOG_PRIORITY_VERBOSE  : return "verbose";
398                     case SDL_LOG_PRIORITY_DEBUG    : return "debug";
399                     case SDL_LOG_PRIORITY_INFO     : return "info";
400                     case SDL_LOG_PRIORITY_WARN     : return "warn";
401                     case SDL_LOG_PRIORITY_ERROR    : return "error";
402                     case SDL_LOG_PRIORITY_CRITICAL : return "critical";
403                     default                        : return "unknown";
404                 }
405             }
406 
407             static string readableCategory(SDL_LogPriority priority) pure
408             {
409                 switch(priority)
410                 {
411                     case SDL_LOG_CATEGORY_APPLICATION : return "application";
412                     case SDL_LOG_CATEGORY_ERROR       : return "error";
413                     case SDL_LOG_CATEGORY_SYSTEM      : return "system";
414                     case SDL_LOG_CATEGORY_AUDIO       : return "audio";
415                     case SDL_LOG_CATEGORY_VIDEO       : return "video";
416                     case SDL_LOG_CATEGORY_RENDER      : return "render";
417                     case SDL_LOG_CATEGORY_INPUT       : return "input";
418                     default                           : return "unknown";
419                 }
420             }
421 
422             string formattedMessage = format("SDL (category %s, priority %s): %s", 
423                                              readableCategory(category), 
424                                              readablePriority(priority), 
425                                              sanitizeUTF8(message, _logger, "SDL logging"));
426 
427             if (priority == SDL_LOG_PRIORITY_WARN)
428                 _logger.warning(formattedMessage);
429             else if (priority == SDL_LOG_PRIORITY_ERROR ||  priority == SDL_LOG_PRIORITY_CRITICAL)
430                 _logger.error(formattedMessage);
431             else
432                 _logger.info(formattedMessage);
433         }
434 
435         SDL_assert_state onLogSDLAssertion(const(SDL_assert_data)* adata)
436         {
437             _logger.warningf("SDL assertion error: %s in %s line %d", adata.condition, adata.filename, adata.linenum);
438 
439             debug 
440                 return SDL_ASSERTION_ABORT; // crash in debug mode
441             else
442                 return SDL_ASSERTION_ALWAYS_IGNORE; // ingore SDL assertions in release
443         }
444 
445         // dispatch to relevant event callbacks
446         void dispatchEvent(const (SDL_Event*) event)
447         {
448             switch(event.type)
449             {
450                 case SDL_WINDOWEVENT:
451                     dispatchWindowEvent(&event.window);
452                     break;
453 
454                 case SDL_KEYDOWN:
455                 case SDL_KEYUP:
456                     const (SDL_KeyboardEvent*) keyboardEvent = &event.key;
457                     SDL2Window* window = (keyboardEvent.windowID in _knownWindows);
458                     if (window !is null)
459                     {
460                         if (event.type == SDL_KEYDOWN)
461                             window.onKeyDown(keyboardEvent.timestamp, _keyboard, keyboardEvent.keysym.sym);
462                         else
463                             window.onKeyUp(keyboardEvent.timestamp, _keyboard, keyboardEvent.keysym.sym);
464                     }
465                     break;
466 
467                 case SDL_MOUSEBUTTONUP:
468                 case SDL_MOUSEBUTTONDOWN:
469                     const (SDL_MouseButtonEvent*) mbEvent = &event.button;
470                     SDL2Window* window = (mbEvent.windowID in _knownWindows);
471                     if (window !is null)
472                     {
473                         if (event.type == SDL_MOUSEBUTTONDOWN)
474                             window.onMouseButtonPressed(mbEvent.timestamp, _mouse, mbEvent.button, mbEvent.clicks > 1);
475                         else
476                             window.onMouseButtonReleased(mbEvent.timestamp, _mouse, mbEvent.button);
477                     }
478                     break;
479 
480                 case SDL_MOUSEWHEEL:
481                     const (SDL_MouseWheelEvent*) wheelEvent = &event.wheel;
482                     SDL2Window* window = (wheelEvent.windowID in _knownWindows);
483                     if (window !is null)
484                         window.onMouseWheel(wheelEvent.timestamp, _mouse, wheelEvent.x, wheelEvent.y);
485                     break;
486 
487                 case SDL_MOUSEMOTION:
488                     const (SDL_MouseMotionEvent*) motionEvent = &event.motion;
489                     SDL2Window* window = (motionEvent.windowID in _knownWindows);
490                     if (window !is null)
491                         window.onMouseMove(motionEvent.timestamp, _mouse);
492                     break;
493 
494                 default:
495                     break;
496             }
497         }
498 
499         // update state based on event
500         // TODO: add joystick state
501         //       add haptic state
502         void updateState(const (SDL_Event*) event)
503         {
504             switch(event.type)
505             {
506                 case SDL_QUIT: 
507                     _quitWasRequested = true;
508                     break;
509 
510                 case SDL_KEYDOWN:
511                 case SDL_KEYUP:
512                     updateKeyboard(&event.key);
513                     break;
514 
515                 case SDL_MOUSEMOTION:
516                     _mouse.updateMotion(&event.motion);
517                 break;
518                 
519                 case SDL_MOUSEBUTTONUP:
520                 case SDL_MOUSEBUTTONDOWN:
521                     _mouse.updateButtons(&event.button);
522                 break;
523 
524                 case SDL_MOUSEWHEEL:
525                     _mouse.updateWheel(&event.wheel);
526                 break;
527 
528                 default:
529                     break;
530             }
531         }
532 
533         // TODO: add window callbacks when pressing a key?
534         void updateKeyboard(const(SDL_KeyboardEvent*) event)
535         {
536             // ignore key-repeat
537             if (event.repeat != 0)
538                 return;
539 
540             switch (event.type)
541             {
542                 case SDL_KEYDOWN:
543                     assert(event.state == SDL_PRESSED);
544                     _keyboard.markKeyAsPressed(event.keysym.scancode);
545                     break;
546 
547                 case SDL_KEYUP:
548                     assert(event.state == SDL_RELEASED);
549                     _keyboard.markKeyAsReleased(event.keysym.scancode);
550                     break;
551 
552                 default:
553                     break;
554             }
555         }
556 
557         // call callbacks that can be overriden by subclassing SDL2Window
558         void dispatchWindowEvent(const (SDL_WindowEvent*) windowEvent)
559         {
560             assert(windowEvent.type == SDL_WINDOWEVENT);
561 
562             SDL2Window* window = (windowEvent.windowID in _knownWindows);
563 
564             if (window is null)
565             {
566                 _logger.warningf("Received a SDL event for an unknown window (id = %s)", windowEvent.windowID);
567                 return; // no such id known, warning
568             }
569 
570             switch (windowEvent.event)
571             {
572                 case SDL_WINDOWEVENT_SHOWN:
573                     window.onShow();
574                     break;
575 
576                 case SDL_WINDOWEVENT_HIDDEN:
577                     window.onHide();
578                     break;
579 
580                 case SDL_WINDOWEVENT_EXPOSED:
581                     window.onExposed();
582                     break;
583 
584                 case SDL_WINDOWEVENT_MOVED:
585                     window.onMove(windowEvent.data1, windowEvent.data2);
586                     break;
587 
588                 case SDL_WINDOWEVENT_RESIZED:
589                     window.onResized(windowEvent.data1, windowEvent.data2);
590                     break;
591 
592                 case SDL_WINDOWEVENT_SIZE_CHANGED:
593                     window.onSizeChanged();
594                     break;
595 
596                 case SDL_WINDOWEVENT_MINIMIZED:
597                     window.onMinimized();
598                     break;
599 
600                 case SDL_WINDOWEVENT_MAXIMIZED:
601                     window.onMaximized();
602                     break;
603 
604                 case SDL_WINDOWEVENT_RESTORED:
605                     window.onRestored();
606                     break;
607 
608                 case SDL_WINDOWEVENT_ENTER:
609                     window.onEnter();
610                     break;
611 
612                 case SDL_WINDOWEVENT_LEAVE:
613                     window.onLeave();
614                     break;
615 
616                 case SDL_WINDOWEVENT_FOCUS_GAINED:
617                     window.onFocusGained();
618                     break;
619 
620                 case SDL_WINDOWEVENT_FOCUS_LOST:
621                     window.onFocusLost();
622                     break;
623 
624                 case SDL_WINDOWEVENT_CLOSE:
625                     window.onClose();
626                     break;
627 
628                 default:
629                     // not a window event
630                     break;
631             }
632         }
633     }
634 }
635 
636 extern(C) private nothrow
637 {
638     void loggingCallbackSDL(void* userData, int category, SDL_LogPriority priority, const(char)* message)
639     {
640         try
641         {
642             SDL2 sdl2 = cast(SDL2)userData;
643 
644             try
645                 sdl2.onLogMessage(category, priority, message);
646             catch (Exception e)
647             {
648                 // got exception while logging, ignore it
649             }
650         }
651         catch (Throwable e)
652         {
653             // No Throwable is supposed to cross C callbacks boundaries
654             // Crash immediately
655             exit(-1);
656         }
657     }
658 
659     SDL_assert_state assertCallbackSDL(const(SDL_assert_data)* data, void* userData)
660     {
661         try
662         {
663             SDL2 sdl2 = cast(SDL2)userData;
664 
665             try
666                 return sdl2.onLogSDLAssertion(data);
667             catch (Exception e)
668             {
669                 // got exception while logging, ignore it
670             }
671         }
672         catch (Throwable e)
673         {
674             // No Throwable is supposed to cross C callbacks boundaries
675             // Crash immediately
676             exit(-1);
677         }
678         return SDL_ASSERTION_ALWAYS_IGNORE;
679     }
680 }
681 
682 final class SDL2DisplayMode
683 {
684     public
685     {
686         this(int modeIndex, SDL_DisplayMode mode)
687         {
688             _modeIndex = modeIndex;
689             _mode = mode;
690         }
691 
692         override string toString()
693         {
694             return format("mode #%s (width = %spx, height = %spx, rate = %shz, format = %s)", 
695                           _modeIndex, _mode.w, _mode.h, _mode.refresh_rate, _mode.format);
696         }
697     }
698 
699     private
700     {
701         int _modeIndex;
702         SDL_DisplayMode _mode;
703     }
704 }
705 
706 final class SDL2VideoDisplay
707 {
708     public
709     {
710         this(int displayindex, SDL_Rect bounds, SDL2DisplayMode[] availableModes)
711         {
712             _displayindex = displayindex;
713             _bounds = bounds;
714             _availableModes = availableModes;
715         }
716 
717         const(SDL2DisplayMode[]) availableModes() pure const nothrow
718         {
719             return _availableModes;
720         }
721 
722         SDL_Point dimension() pure const nothrow
723         {
724             return SDL_Point(_bounds.w, _bounds.h);
725         }
726 
727         SDL_Rect bounds() pure const nothrow
728         {
729             return _bounds;
730         }
731 
732         override string toString()
733         {
734             string res = format("display #%s (start = %s,%s - dimension = %s x %s)\n", _displayindex, 
735                                 _bounds.x, _bounds.y, _bounds.w, _bounds.h);
736             foreach (mode; _availableModes)
737                 res ~= format("  - %s\n", mode);
738             return res;
739         }
740     }
741 
742     private
743     {
744         int _displayindex;
745         SDL2DisplayMode[] _availableModes;
746         SDL_Rect _bounds;
747     }
748 }
749