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.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 
303     package
304     {
305         Logger _logger;
306 
307         // exception mechanism that shall be used by every module here
308         void throwSDL2Exception(string callThatFailed)
309         {
310             string message = format("%s failed: %s", callThatFailed, getErrorString());
311             throw new SDL2Exception(message);
312         }
313 
314         // return last SDL error and clears it
315         string getErrorString()
316         {
317             const(char)* message = SDL_GetError();
318             SDL_ClearError(); // clear error
319             return sanitizeUTF8(message, _logger, "SDL error string");
320         }
321 
322         void registerWindow(SDL2Window window)
323         {
324             _knownWindows[window.id()] = window;
325         }
326 
327         void unregisterWindow(SDL2Window window)
328         {
329             assert((window.id() in _knownWindows) !is null);
330             _knownWindows.remove(window.id());
331         }        
332     }
333 
334     private
335     {
336         bool _SDL2LoggingRedirected;
337         SDL_LogOutputFunction _previousLogCallback;
338         void* _previousLogUserdata;
339 
340 
341         bool _SDLInitialized;
342 
343         // all created windows are keeped in this map
344         // to be able to dispatch event
345         SDL2Window[uint] _knownWindows;
346 
347         // SDL_QUIT was received
348         bool _quitWasRequested = false;
349 
350         // Holds keyboard state
351         SDL2Keyboard _keyboard;
352 
353         // Holds mouse state
354         SDL2Mouse _mouse;
355 
356         bool subSystemInitialized(int subSystem)
357         {
358             int inited = SDL_WasInit(SDL_INIT_EVERYTHING);
359             return 0 != ( inited & subSystem );
360         }
361 
362         void subSystemInit(int flag)
363         {
364             if (!subSystemInitialized(flag))
365             {
366                 int res = SDL_InitSubSystem(flag);
367                 if (0 != res)
368                     throwSDL2Exception("SDL_InitSubSystem");
369             }
370         }
371 
372         void onLogMessage(int category, SDL_LogPriority priority, const(char)* message)
373         {
374             static string readablePriority(SDL_LogPriority priority) pure
375             {
376                 switch(priority)
377                 {
378                     case SDL_LOG_PRIORITY_VERBOSE  : return "verbose";
379                     case SDL_LOG_PRIORITY_DEBUG    : return "debug";
380                     case SDL_LOG_PRIORITY_INFO     : return "info";
381                     case SDL_LOG_PRIORITY_WARN     : return "warn";
382                     case SDL_LOG_PRIORITY_ERROR    : return "error";
383                     case SDL_LOG_PRIORITY_CRITICAL : return "critical";
384                     default                        : return "unknown";
385                 }
386             }
387 
388             static string readableCategory(SDL_LogPriority priority) pure
389             {
390                 switch(priority)
391                 {
392                     case SDL_LOG_CATEGORY_APPLICATION : return "application";
393                     case SDL_LOG_CATEGORY_ERROR       : return "error";
394                     case SDL_LOG_CATEGORY_SYSTEM      : return "system";
395                     case SDL_LOG_CATEGORY_AUDIO       : return "audio";
396                     case SDL_LOG_CATEGORY_VIDEO       : return "video";
397                     case SDL_LOG_CATEGORY_RENDER      : return "render";
398                     case SDL_LOG_CATEGORY_INPUT       : return "input";
399                     default                           : return "unknown";
400                 }
401             }
402 
403             string formattedMessage = format("SDL (category %s, priority %s): %s", 
404                                              readableCategory(category), 
405                                              readablePriority(priority), 
406                                              sanitizeUTF8(message, _logger, "SDL logging"));
407 
408             if (priority == SDL_LOG_PRIORITY_WARN)
409                 _logger.warning(formattedMessage);
410             else if (priority == SDL_LOG_PRIORITY_ERROR ||  priority == SDL_LOG_PRIORITY_CRITICAL)
411                 _logger.error(formattedMessage);
412             else
413                 _logger.info(formattedMessage);
414         }
415 
416         SDL_assert_state onLogSDLAssertion(const(SDL_assert_data)* adata)
417         {
418             _logger.warningf("SDL assertion error: %s in %s line %d", adata.condition, adata.filename, adata.linenum);
419 
420             debug 
421                 return SDL_ASSERTION_ABORT; // crash in debug mode
422             else
423                 return SDL_ASSERTION_ALWAYS_IGNORE; // ingore SDL assertions in release
424         }
425 
426         // dispatch to relevant event callbacks
427         void dispatchEvent(const (SDL_Event*) event)
428         {
429             switch(event.type)
430             {
431                 case SDL_WINDOWEVENT:
432                     dispatchWindowEvent(&event.window);
433                     break;
434 
435                 default:
436                     break;
437             }
438         }
439 
440         // update state based on event
441         // TODO: add mouse state
442         //       add joystick state
443         //       add haptic state
444         void updateState(const (SDL_Event*) event)
445         {
446             switch(event.type)
447             {
448                 case SDL_QUIT: 
449                     _quitWasRequested = true;
450                     break;
451 
452                 case SDL_KEYDOWN:
453                 case SDL_KEYUP:
454                     updateKeyboard(&event.key);
455                     break;
456 
457                 case SDL_MOUSEMOTION:
458                     _mouse.updateMotion(&event.motion);
459                 break;
460                 
461                 case SDL_MOUSEBUTTONUP:
462                 case SDL_MOUSEBUTTONDOWN:
463                     _mouse.updateButtons(&event.button);
464                 break;
465 
466                 case SDL_MOUSEWHEEL:
467                     _mouse.updateWheel(&event.wheel);
468                 break;
469 
470                 default:
471                     break;
472             }
473         }
474 
475         // TODO: add window callbacks when pressing a key?
476         void updateKeyboard(const(SDL_KeyboardEvent*) event)
477         {
478             // ignore key-repeat
479             if (event.repeat != 0)
480                 return;
481 
482             switch (event.type)
483             {
484                 case SDL_KEYDOWN:
485                     assert(event.state == SDL_PRESSED);
486                     _keyboard.markKeyAsPressed(event.keysym.scancode);
487                     break;
488 
489                 case SDL_KEYUP:
490                     assert(event.state == SDL_RELEASED);
491                     _keyboard.markKeyAsReleased(event.keysym.scancode);
492                     break;
493 
494                 default:
495                     break;
496             }
497         }
498 
499         // call callbacks that can be overriden by subclassing SDL2Window
500         void dispatchWindowEvent(const (SDL_WindowEvent*) windowEvent)
501         {
502             assert(windowEvent.type == SDL_WINDOWEVENT);
503 
504             SDL2Window* window = (windowEvent.windowID in _knownWindows);
505 
506             if (window is null)
507             {
508                 _logger.warningf("Received a SDL event for an unknown window (id = %s)", windowEvent.windowID);
509                 return; // no such id known, warning
510             }
511 
512             switch (windowEvent.event)
513             {
514                 case SDL_WINDOWEVENT_SHOWN:
515                     window.onShow();
516                     break;
517 
518                 case SDL_WINDOWEVENT_HIDDEN:
519                     window.onHide();
520                     break;
521 
522                 case SDL_WINDOWEVENT_EXPOSED:
523                     window.onExposed();
524                     break;
525 
526                 case SDL_WINDOWEVENT_MOVED:
527                     window.onMove(windowEvent.data1, windowEvent.data2);
528                     break;
529 
530                 case SDL_WINDOWEVENT_RESIZED:
531                     window.onResized(windowEvent.data1, windowEvent.data2);
532                     break;
533 
534                 case SDL_WINDOWEVENT_SIZE_CHANGED:
535                     window.onSizeChanged();
536                     break;
537 
538                 case SDL_WINDOWEVENT_MINIMIZED:
539                     window.onMinimized();
540                     break;
541 
542                 case SDL_WINDOWEVENT_MAXIMIZED:
543                     window.onMaximized();
544                     break;
545 
546                 case SDL_WINDOWEVENT_RESTORED:
547                     window.onRestored();
548                     break;
549 
550                 case SDL_WINDOWEVENT_ENTER:
551                     window.onEnter();
552                     break;
553 
554                 case SDL_WINDOWEVENT_LEAVE:
555                     window.onLeave();
556                     break;
557 
558                 case SDL_WINDOWEVENT_FOCUS_GAINED:
559                     window.onFocusGained();
560                     break;
561 
562                 case SDL_WINDOWEVENT_FOCUS_LOST:
563                     window.onFocusLost();
564                     break;
565 
566                 case SDL_WINDOWEVENT_CLOSE:
567                     window.onClose();
568                     break;
569 
570                 default:
571                     // not a window event
572                     break;
573             }
574         }
575     }
576 }
577 
578 extern(C) private nothrow
579 {
580     void loggingCallbackSDL(void* userData, int category, SDL_LogPriority priority, const(char)* message)
581     {
582         try
583         {
584             SDL2 sdl2 = cast(SDL2)userData;
585 
586             try
587                 sdl2.onLogMessage(category, priority, message);
588             catch (Exception e)
589             {
590                 // got exception while logging, ignore it
591             }
592         }
593         catch (Throwable e)
594         {
595             // No Throwable is supposed to cross C callbacks boundaries
596             // Crash immediately
597             exit(-1);
598         }
599     }
600 
601     SDL_assert_state assertCallbackSDL(const(SDL_assert_data)* data, void* userData)
602     {
603         try
604         {
605             SDL2 sdl2 = cast(SDL2)userData;
606 
607             try
608                 return sdl2.onLogSDLAssertion(data);
609             catch (Exception e)
610             {
611                 // got exception while logging, ignore it
612             }
613         }
614         catch (Throwable e)
615         {
616             // No Throwable is supposed to cross C callbacks boundaries
617             // Crash immediately
618             exit(-1);
619         }
620         return SDL_ASSERTION_ALWAYS_IGNORE;
621     }
622 }
623 
624 final class SDL2DisplayMode
625 {
626     public
627     {
628         this(int modeIndex, SDL_DisplayMode mode)
629         {
630             _modeIndex = modeIndex;
631             _mode = mode;
632         }
633 
634         override string toString()
635         {
636             return format("mode #%s (width = %spx, height = %spx, rate = %shz, format = %s)", 
637                           _modeIndex, _mode.w, _mode.h, _mode.refresh_rate, _mode.format);
638         }
639     }
640 
641     private
642     {
643         int _modeIndex;
644         SDL_DisplayMode _mode;
645     }
646 }
647 
648 final class SDL2VideoDisplay
649 {
650     public
651     {
652         this(int displayindex, SDL_Rect bounds, SDL2DisplayMode[] availableModes)
653         {
654             _displayindex = displayindex;
655             _bounds = bounds;
656             _availableModes = availableModes;
657         }
658 
659         const(SDL2DisplayMode[]) availableModes() pure const nothrow
660         {
661             return _availableModes;
662         }
663 
664         SDL_Point dimension() pure const nothrow
665         {
666             return SDL_Point(_bounds.w, _bounds.h);
667         }
668 
669         SDL_Rect bounds() pure const nothrow
670         {
671             return _bounds;
672         }
673 
674         override string toString()
675         {
676             string res = format("display #%s (start = %s,%s - dimension = %s x %s)\n", _displayindex, 
677                                 _bounds.x, _bounds.y, _bounds.w, _bounds.h);
678             foreach (mode; _availableModes)
679                 res ~= format("  - %s\n", mode);
680             return res;
681         }
682     }
683 
684     private
685     {
686         int _displayindex;
687         SDL2DisplayMode[] _availableModes;
688         SDL_Rect _bounds;
689     }
690 }
691