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