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        derelict.util.loader;
13 
14 import std.experimental.logger;
15 
16 import 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         /// You can specify a minimum version of SDL2 you wish your project to support.
44         /// Creating this object doesn't initialize any SDL subsystem!
45         /// Params:
46         ///     logger         = The logger to redirect logging to.
47         ///     sdl2Version    = The version of SDL2 to load. Defaults to SharedLibVersion(2, 0, 2).
48         /// Throws: $(D SDL2Exception) on error.
49         /// See_also: $(LINK http://wiki.libsdl.org/SDL_Init), $(D subSystemInit)
50         this(Logger logger, SharedLibVersion sdl2Version = SharedLibVersion(2, 0, 2))
51         {
52             _logger = logger is null ? new NullLogger() : logger;
53             _SDLInitialized = false;
54             _SDL2LoggingRedirected = false;
55             try
56             {
57                 DerelictSDL2.load(sdl2Version);
58             }
59             catch(DerelictException e)
60             {
61                 throw new SDL2Exception(e.msg);
62             }
63 
64             // enable all logging, and pipe it to our own logger object
65             {
66                 SDL_LogGetOutputFunction(_previousLogCallback, &_previousLogUserdata);
67                 SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE);
68                 SDL_LogSetOutputFunction(&loggingCallbackSDL, cast(void*)this);
69 
70                 SDL_SetAssertionHandler(&assertCallbackSDL, cast(void*)this);
71                 _SDL2LoggingRedirected = true;
72             }
73 
74             if (0 != SDL_Init(0))
75                 throwSDL2Exception("SDL_Init");
76 
77             _keyboard = new SDL2Keyboard(this);
78             _mouse = new SDL2Mouse(this);
79         }
80 
81         /// Releases the SDL library and all resources.
82         /// All resources should have been released at this point,
83         /// since you won't be able to call any SDL function afterwards.
84         /// See_also: $(LINK http://wiki.libsdl.org/SDL_Quit)
85         void close()
86         {
87             // restore previously set logging function
88             if (_SDL2LoggingRedirected)
89             {
90                 SDL_LogSetOutputFunction(_previousLogCallback, _previousLogUserdata);
91                 _SDL2LoggingRedirected = false;
92 
93                 SDL_SetAssertionHandler(null, cast(void*)this);
94             }
95 
96             if (_SDLInitialized)
97             {
98                 SDL_Quit();
99                 _SDLInitialized = false;
100             }
101 
102             if (DerelictSDL2.isLoaded())
103                 DerelictSDL2.unload();
104         }
105 
106         ~this()
107         {
108             close();
109         }
110 
111         /// Returns: true if a subsystem is initialized.
112         /// See_also: $(LINK http://wiki.libsdl.org/SDL_WasInit)
113         bool subSystemInitialized(int subSystem)
114         {
115             int inited = SDL_WasInit(SDL_INIT_EVERYTHING);
116             return 0 != (inited & subSystem);
117         }
118 
119         /// Initialize a subsystem. By default, all SDL subsystems are uninitialized.
120         /// See_also: $(LINK http://wiki.libsdl.org/SDL_InitSubSystem)
121         void subSystemInit(int flag)
122         {
123             if (!subSystemInitialized(flag))
124             {
125                 int res = SDL_InitSubSystem(flag);
126                 if (0 != res)
127                     throwSDL2Exception("SDL_InitSubSystem");
128             }
129         }
130 
131         /// Returns: Available displays information.
132         /// Throws: $(D SDL2Exception) on error.
133         SDL2VideoDisplay[] getDisplays()
134         {
135             int numDisplays = SDL_GetNumVideoDisplays();
136 
137             SDL2VideoDisplay[] availableDisplays;
138 
139             for (int displayIndex = 0; displayIndex < numDisplays; ++displayIndex)
140             {
141                 SDL_Rect rect;
142                 int res = SDL_GetDisplayBounds(displayIndex, &rect);
143                 if (res != 0)
144                     throwSDL2Exception("SDL_GetDisplayBounds");
145 
146                 SDL2DisplayMode[] availableModes;
147 
148                 int numModes = SDL_GetNumDisplayModes(displayIndex);
149                 for(int modeIndex = 0; modeIndex < numModes; ++modeIndex)
150                 {
151                     SDL_DisplayMode mode;
152                     if (0 != SDL_GetDisplayMode(displayIndex, modeIndex, &mode))
153                         throwSDL2Exception("SDL_GetDisplayMode");
154 
155                     availableModes ~= new SDL2DisplayMode(modeIndex, mode);
156                 }
157 
158                 availableDisplays ~= new SDL2VideoDisplay(displayIndex, rect, availableModes);
159             }
160             return availableDisplays;
161         }
162 
163         /// Returns: Resolution of the first display.
164         /// Throws: $(D SDL2Exception) on error.
165         SDL_Point firstDisplaySize()
166         {
167             auto displays = getDisplays();
168             if (displays.length == 0)
169                 throw new SDL2Exception("no display");
170             return displays[0].dimension();
171         }
172 
173         /// Returns: Available renderers information.
174         /// Throws: $(D SDL2Exception) on error.
175         SDL2RendererInfo[] getRenderersInfo()
176         {
177             SDL2RendererInfo[] res;
178             int num = SDL_GetNumRenderDrivers();
179             if (num < 0)
180                 throwSDL2Exception("SDL_GetNumRenderDrivers");
181 
182             for (int i = 0; i < num; ++i)
183             {
184                 SDL_RendererInfo info;
185                 int err = SDL_GetRenderDriverInfo(i, &info);
186                 if (err != 0)
187                     throwSDL2Exception("SDL_GetRenderDriverInfo");
188                 res ~= new SDL2RendererInfo(info);
189             }
190             return res;
191         }
192 
193         /// Get next SDL event.
194         /// Input state gets updated and window callbacks are called too.
195         /// Returns: true if returned an event.
196         bool pollEvent(SDL_Event* event)
197         {
198             if (SDL_PollEvent(event) != 0)
199             {
200                 updateState(event);
201                 return true;
202             }
203             else
204                 return false;
205         }
206 
207         /// Wait for next SDL event.
208         /// Input state gets updated and window callbacks are called too.
209         /// See_also: $(LINK http://wiki.libsdl.org/SDL_WaitEvent)
210         /// Throws: $(D SDL2Exception) on error.
211         void waitEvent(SDL_Event* event)
212         {
213             int res = SDL_WaitEvent(event);
214             if (res == 0)
215                 throwSDL2Exception("SDL_WaitEvent");
216             updateState(event);
217         }
218 
219         /// Wait for next SDL event, with a timeout.
220         /// Input state gets updated and window callbacks are called too.
221         /// See_also: $(LINK http://wiki.libsdl.org/SDL_WaitEventTimeout)
222         /// Throws: $(D SDL2Exception) on error.
223         /// Returns: true if returned an event.
224         bool waitEventTimeout(SDL_Event* event, int timeoutMs)
225         {
226             //  "This also returns 0 if the timeout elapsed without an event arriving."
227             // => no way to separate errors from no event, error code is ignored
228             int res = SDL_WaitEventTimeout(event, timeoutMs);
229             if (res == 1)
230             {
231                 updateState(event);
232                 return true;
233             }
234             else
235                 return false;
236         }
237 
238         /// Process all pending SDL events.
239         /// Input state gets updated. You would typically look at event instead of calling
240         /// this function.
241         /// See_also: $(D pollEvent), $(D waitEvent), $(D waitEventTimeout)
242         void processEvents()
243         {
244             SDL_Event event;
245             while(SDL_PollEvent(&event) != 0)
246                 updateState(&event);
247         }
248 
249         /// Returns: Keyboard state.
250         /// The keyboard state is updated by processEvents() and pollEvent().
251         SDL2Keyboard keyboard()
252         {
253             return _keyboard;
254         }
255 
256         /// Returns: Mouse state.
257         /// The mouse state is updated by processEvents() and pollEvent().
258         SDL2Mouse mouse()
259         {
260             return _mouse;
261         }
262 
263         /// Returns: true if an application termination has been requested.
264         bool wasQuitRequested() const
265         {
266             return _quitWasRequested;
267         }
268 
269         /// Start text input.
270         void startTextInput()
271         {
272             SDL_StartTextInput();
273         }
274 
275         /// Stops text input.
276         void stopTextInput()
277         {
278             SDL_StopTextInput();
279         }
280 
281         /// Sets clipboard content.
282         /// Throws: $(D SDL2Exception) on error.
283         string setClipboard(string s)
284         {
285             int err = SDL_SetClipboardText(toStringz(s));
286             if (err != 0)
287                 throwSDL2Exception("SDL_SetClipboardText");
288             return s;
289         }
290 
291         /// Returns: Clipboard content.
292         /// Throws: $(D SDL2Exception) on error.
293         const(char)[] getClipboard()
294         {
295             if (SDL_HasClipboardText() == SDL_FALSE)
296                 return null;
297 
298             const(char)* s = SDL_GetClipboardText();
299             if (s is null)
300                 throwSDL2Exception("SDL_GetClipboardText");
301 
302             return fromStringz(s);
303         }
304 
305         /// Returns: Available SDL video drivers.
306         const(char)[][] getVideoDrivers()
307         {
308             const int numDrivers = SDL_GetNumVideoDrivers();
309             const(char)[][] res;
310             res.length = numDrivers;
311             for(int i = 0; i < numDrivers; ++i)
312                 res[i] = fromStringz(SDL_GetVideoDriver(i));
313             return res;
314         }
315 
316         /// Returns: Platform name.
317         const(char)[] getPlatform()
318         {
319             return fromStringz(SDL_GetPlatform());
320         }
321 
322         /// Returns: L1 cacheline size in bytes.
323         int getL1LineSize()
324         {
325             int res = SDL_GetCPUCacheLineSize();
326             if (res <= 0)
327                 res = 64;
328             return res;
329         }
330 
331         /// Returns: number of CPUs.
332         int getCPUCount()
333         {
334             int res = SDL_GetCPUCount();
335             if (res <= 0)
336                 res = 1;
337             return res;
338         }
339 
340         /// Returns: A path suitable for writing configuration files, saved games, etc...
341         /// See_also: $(LINK http://wiki.libsdl.org/SDL_GetPrefPath)
342         /// Throws: $(D SDL2Exception) on error.
343         const(char)[] getPrefPath(string orgName, string applicationName)
344         {
345             char* basePath = SDL_GetPrefPath(toStringz(orgName), toStringz(applicationName));
346             if (basePath != null)
347             {
348                 const(char)[] result = fromStringz(basePath);
349                 SDL_free(basePath);
350                 return result;
351             }
352             else
353             {
354                 throwSDL2Exception("SDL_GetPrefPath");
355                 return null; // unreachable
356             }
357         }
358     }
359 
360     package
361     {
362         Logger _logger;
363 
364         // exception mechanism that shall be used by every module here
365         void throwSDL2Exception(string callThatFailed)
366         {
367             string message = format("%s failed: %s", callThatFailed, getErrorString());
368             throw new SDL2Exception(message);
369         }
370 
371         // return last SDL error and clears it
372         const(char)[] getErrorString()
373         {
374             const(char)* message = SDL_GetError();
375             SDL_ClearError(); // clear error
376             return fromStringz(message);
377         }
378     }
379 
380     private
381     {
382         bool _SDL2LoggingRedirected;
383         SDL_LogOutputFunction _previousLogCallback;
384         void* _previousLogUserdata;
385 
386 
387         bool _SDLInitialized;
388 
389         // all created windows are keeped in this map
390         // to be able to dispatch event
391         SDL2Window[uint] _knownWindows;
392 
393         // SDL_QUIT was received
394         bool _quitWasRequested = false;
395 
396         // Holds keyboard state
397         SDL2Keyboard _keyboard;
398 
399         // Holds mouse state
400         SDL2Mouse _mouse;
401 
402         void onLogMessage(int category, SDL_LogPriority priority, const(char)* message)
403         {
404             static string readablePriority(SDL_LogPriority priority) pure
405             {
406                 switch(priority)
407                 {
408                     case SDL_LOG_PRIORITY_VERBOSE  : return "verbose";
409                     case SDL_LOG_PRIORITY_DEBUG    : return "debug";
410                     case SDL_LOG_PRIORITY_INFO     : return "info";
411                     case SDL_LOG_PRIORITY_WARN     : return "warn";
412                     case SDL_LOG_PRIORITY_ERROR    : return "error";
413                     case SDL_LOG_PRIORITY_CRITICAL : return "critical";
414                     default                        : return "unknown";
415                 }
416             }
417 
418             static string readableCategory(SDL_LogPriority priority) pure
419             {
420                 switch(priority)
421                 {
422                     case SDL_LOG_CATEGORY_APPLICATION : return "application";
423                     case SDL_LOG_CATEGORY_ERROR       : return "error";
424                     case SDL_LOG_CATEGORY_SYSTEM      : return "system";
425                     case SDL_LOG_CATEGORY_AUDIO       : return "audio";
426                     case SDL_LOG_CATEGORY_VIDEO       : return "video";
427                     case SDL_LOG_CATEGORY_RENDER      : return "render";
428                     case SDL_LOG_CATEGORY_INPUT       : return "input";
429                     default                           : return "unknown";
430                 }
431             }
432 
433             string formattedMessage = format("SDL (category %s, priority %s): %s",
434                                              readableCategory(category),
435                                              readablePriority(priority),
436                                              fromStringz(message));
437 
438             if (priority == SDL_LOG_PRIORITY_WARN)
439                 _logger.warning(formattedMessage);
440             else if (priority == SDL_LOG_PRIORITY_ERROR ||  priority == SDL_LOG_PRIORITY_CRITICAL)
441                 _logger.error(formattedMessage);
442             else
443                 _logger.info(formattedMessage);
444         }
445 
446         SDL_assert_state onLogSDLAssertion(const(SDL_assert_data)* adata)
447         {
448             _logger.warningf("SDL assertion error: %s in %s line %d", adata.condition, adata.filename, adata.linenum);
449 
450             debug
451                 return SDL_ASSERTION_ABORT; // crash in debug mode
452             else
453                 return SDL_ASSERTION_ALWAYS_IGNORE; // ingore SDL assertions in release
454         }
455 
456         // update state based on event
457         // TODO: add joystick state
458         //       add haptic state
459         void updateState(const (SDL_Event*) event)
460         {
461             switch(event.type)
462             {
463                 case SDL_QUIT:
464                     _quitWasRequested = true;
465                     break;
466 
467                 case SDL_KEYDOWN:
468                 case SDL_KEYUP:
469                     updateKeyboard(&event.key);
470                     break;
471 
472                 case SDL_MOUSEMOTION:
473                     _mouse.updateMotion(&event.motion);
474                 break;
475 
476                 case SDL_MOUSEBUTTONUP:
477                 case SDL_MOUSEBUTTONDOWN:
478                     _mouse.updateButtons(&event.button);
479                 break;
480 
481                 case SDL_MOUSEWHEEL:
482                     _mouse.updateWheel(&event.wheel);
483                 break;
484 
485                 default:
486                     break;
487             }
488         }
489 
490         void updateKeyboard(const(SDL_KeyboardEvent*) event)
491         {
492             // ignore key-repeat
493             if (event.repeat != 0)
494                 return;
495 
496             switch (event.type)
497             {
498                 case SDL_KEYDOWN:
499                     assert(event.state == SDL_PRESSED);
500                     _keyboard.markKeyAsPressed(event.keysym.scancode);
501                     break;
502 
503                 case SDL_KEYUP:
504                     assert(event.state == SDL_RELEASED);
505                     _keyboard.markKeyAsReleased(event.keysym.scancode);
506                     break;
507 
508                 default:
509                     break;
510             }
511         }
512     }
513 }
514 
515 extern(C) private nothrow
516 {
517     void loggingCallbackSDL(void* userData, int category, SDL_LogPriority priority, const(char)* message)
518     {
519         try
520         {
521             SDL2 sdl2 = cast(SDL2)userData;
522 
523             try
524                 sdl2.onLogMessage(category, priority, message);
525             catch (Exception e)
526             {
527                 // got exception while logging, ignore it
528             }
529         }
530         catch (Throwable e)
531         {
532             // No Throwable is supposed to cross C callbacks boundaries
533             // Crash immediately
534             exit(-1);
535         }
536     }
537 
538     SDL_assert_state assertCallbackSDL(const(SDL_assert_data)* data, void* userData)
539     {
540         try
541         {
542             SDL2 sdl2 = cast(SDL2)userData;
543 
544             try
545                 return sdl2.onLogSDLAssertion(data);
546             catch (Exception e)
547             {
548                 // got exception while logging, ignore it
549             }
550         }
551         catch (Throwable e)
552         {
553             // No Throwable is supposed to cross C callbacks boundaries
554             // Crash immediately
555             exit(-1);
556         }
557         return SDL_ASSERTION_ALWAYS_IGNORE;
558     }
559 }
560 
561 final class SDL2DisplayMode
562 {
563     public
564     {
565         this(int modeIndex, SDL_DisplayMode mode)
566         {
567             _modeIndex = modeIndex;
568             _mode = mode;
569         }
570 
571         override string toString()
572         {
573             return format("mode #%s (width = %spx, height = %spx, rate = %shz, format = %s)",
574                           _modeIndex, _mode.w, _mode.h, _mode.refresh_rate, _mode.format);
575         }
576     }
577 
578     private
579     {
580         int _modeIndex;
581         SDL_DisplayMode _mode;
582     }
583 }
584 
585 final class SDL2VideoDisplay
586 {
587     public
588     {
589         this(int displayindex, SDL_Rect bounds, SDL2DisplayMode[] availableModes)
590         {
591             _displayindex = displayindex;
592             _bounds = bounds;
593             _availableModes = availableModes;
594         }
595 
596         const(SDL2DisplayMode[]) availableModes() pure const nothrow
597         {
598             return _availableModes;
599         }
600 
601         SDL_Point dimension() pure const nothrow
602         {
603             return SDL_Point(_bounds.w, _bounds.h);
604         }
605 
606         SDL_Rect bounds() pure const nothrow
607         {
608             return _bounds;
609         }
610 
611         override string toString()
612         {
613             string res = format("display #%s (start = %s,%s - dimension = %s x %s)\n", _displayindex,
614                                 _bounds.x, _bounds.y, _bounds.w, _bounds.h);
615             foreach (mode; _availableModes)
616                 res ~= format("  - %s\n", mode);
617             return res;
618         }
619     }
620 
621     private
622     {
623         int _displayindex;
624         SDL2DisplayMode[] _availableModes;
625         SDL_Rect _bounds;
626     }
627 }
628