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