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