1 /** 2 * ae.ui.shell.sdl2.shell 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.ui.shell.sdl2.shell; 15 16 import std.conv; 17 import std.string; 18 19 import derelict.sdl2.sdl; 20 import derelict.util.loader; 21 22 import ae.ui.shell.shell; 23 //import ae.ui.video.video; 24 import ae.ui.video.sdl2common.video; 25 import ae.ui.app.application; 26 public import ae.ui.shell.events; 27 import ae.ui.timer.timer; 28 29 //!!version(Posix) pragma(lib, "dl"); // for Derelict 30 31 /// `Shell` implementation using SDL2. 32 final class SDL2Shell : Shell 33 { 34 Application application; /// 35 SDL2CommonVideo sdlVideo; /// 36 37 this(Application application) 38 { 39 this.application = application; 40 41 //!!SharedLibLoader.disableAutoUnload(); // SDL MM timers may crash on exit 42 DerelictSDL2.load(); 43 auto components = SDL_INIT_VIDEO | SDL_INIT_TIMER; 44 if (application.needSound()) 45 components |= SDL_INIT_AUDIO; 46 if (application.needJoystick()) 47 components |= SDL_INIT_JOYSTICK; 48 sdlEnforce(SDL_Init(components)==0); 49 50 //!!SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL); 51 //!!SDL_EnableUNICODE(1); 52 53 if (application.needJoystick() && SDL_NumJoysticks()) 54 { 55 SDL_JoystickEventState(SDL_ENABLE); 56 SDL_JoystickOpen(0); 57 } 58 } /// 59 60 /// A version of SDL_WaitEvent that sleeps less than 10ms at a time. 61 private int waitEvent() 62 { 63 while (true) 64 { 65 synchronized(this) 66 if (mainThreadQueue.length) 67 return 1; 68 69 SDL_PumpEvents(); 70 switch (SDL_PeepEvents(null, 1, SDL_GETEVENT, 0, uint.max)) 71 { 72 case -1: return 0; 73 case 0: SDL_Delay(1); break; 74 default: return 1; 75 } 76 } 77 } 78 79 override void run() 80 { 81 assert(video, "Video object not set"); 82 sdlVideo = cast(SDL2CommonVideo)video; 83 assert(sdlVideo, "Video is non-SDL"); 84 85 if (audio) 86 audio.start(application); 87 88 video.errorCallback = AppCallback(&quit); 89 quitting = false; 90 91 // video (re-)initialization loop 92 while (!quitting) 93 { 94 reinitPending = false; 95 96 // start renderer 97 video.start(application); 98 99 // The main purpose of this call is to allow the application 100 // to react to window size changes. 101 application.handleInit(); 102 103 // pump events 104 while (!reinitPending && !quitting) 105 { 106 sdlEnforce(waitEvent()); 107 108 synchronized(application) 109 { 110 if (mainThreadQueue.length) 111 { 112 foreach (fn; mainThreadQueue) 113 if (fn) 114 fn(); 115 mainThreadQueue = null; 116 } 117 118 SDL_Event event = void; 119 while (SDL_PollEvent(&event)) 120 handleEvent(&event); 121 } 122 } 123 124 // wait for renderer to stop 125 video.stop(); 126 } 127 128 if (audio) 129 audio.stop(); 130 } /// 131 132 ~this() @nogc 133 { 134 SDL_Quit(); 135 } 136 137 private void delegate()[] mainThreadQueue; 138 139 private void runInMainThread(void delegate() fn) 140 { 141 synchronized(this) 142 mainThreadQueue ~= fn; 143 } 144 145 override void prod() 146 { 147 runInMainThread(null); 148 } /// 149 150 override void setCaption(string caption) 151 { 152 runInMainThread({ 153 static string oldCaption; 154 if (caption != oldCaption) 155 { 156 oldCaption = caption; 157 SDL_SetWindowTitle(sdlVideo.window, toStringz(caption)); 158 } 159 }); 160 } /// 161 162 /// Translate an SDL button index to a `MouseButton`. 163 MouseButton translateMouseButton(ubyte sdlButton) 164 { 165 switch (sdlButton) 166 { 167 case SDL_BUTTON_LEFT: 168 return MouseButton.Left; 169 case SDL_BUTTON_MIDDLE: 170 default: 171 return MouseButton.Middle; 172 case SDL_BUTTON_RIGHT: 173 return MouseButton.Right; 174 } 175 } 176 177 /// Highest SDL_BUTTON constant. 178 enum SDL_BUTTON_LAST = SDL_BUTTON_X2; 179 180 /// Translate an SDL buttons mask to a `MouseButtons`. 181 MouseButtons translateMouseButtons(uint sdlButtons) 182 { 183 MouseButtons result; 184 for (ubyte i=SDL_BUTTON_LEFT; i<=SDL_BUTTON_LAST; i++) 185 if (sdlButtons & SDL_BUTTON(i)) 186 result |= 1<<translateMouseButton(i); 187 return result; 188 } 189 190 /// Handle a single `SDL_Event`. 191 void handleEvent(SDL_Event* event) 192 { 193 switch (event.type) 194 { 195 case SDL_KEYDOWN: 196 /+if ( event.key.keysym.scancode == SDL_SCANCODE_RETURN && (keypressed[SDL_SCANCODE_RALT] || keypressed[SDL_SCANCODE_LALT])) 197 { 198 if (application.toggleFullScreen()) 199 { 200 video.stop(); 201 video.initialize(); 202 video.start(); 203 return false; 204 } 205 }+/ 206 return application.handleKeyDown(sdlKeys[event.key.keysym.scancode], /*event.key.keysym.unicode*/event.key.keysym.sym); // TODO: Use SDL_TextInputEvent 207 case SDL_KEYUP: 208 return application.handleKeyUp(sdlKeys[event.key.keysym.scancode]); 209 210 case SDL_MOUSEBUTTONDOWN: 211 return application.handleMouseDown(event.button.x, event.button.y, translateMouseButton(event.button.button)); 212 case SDL_MOUSEBUTTONUP: 213 return application.handleMouseUp(event.button.x, event.button.y, translateMouseButton(event.button.button)); 214 case SDL_MOUSEMOTION: 215 return application.handleMouseMove(event.motion.x, event.motion.y, translateMouseButtons(event.motion.state)); 216 217 case SDL_JOYAXISMOTION: 218 return application.handleJoyAxisMotion(event.jaxis.axis, cast(short)event.jaxis.value); 219 case SDL_JOYHATMOTION: 220 return application.handleJoyHatMotion (event.jhat.hat, cast(JoystickHatState)event.jhat.value); 221 case SDL_JOYBUTTONDOWN: 222 return application.handleJoyButtonDown(event.jbutton.button); 223 case SDL_JOYBUTTONUP: 224 return application.handleJoyButtonUp (event.jbutton.button); 225 226 case SDL_WINDOWEVENT: 227 switch (event.window.event) 228 { 229 case SDL_WINDOWEVENT_MOVED: 230 auto settings = application.getShellSettings(); 231 settings.windowPosX = event.window.data1; 232 settings.windowPosY = event.window.data2; 233 application.setShellSettings(settings); 234 break; 235 case SDL_WINDOWEVENT_SIZE_CHANGED: 236 auto settings = application.getShellSettings(); 237 settings.windowSizeX = event.window.data1; 238 settings.windowSizeY = event.window.data2; 239 application.setShellSettings(settings); 240 reinitPending = true; 241 break; 242 case SDL_WINDOWEVENT_CLOSE: 243 event.type = SDL_QUIT; 244 SDL_PushEvent(event); 245 break; 246 default: 247 break; 248 } 249 break; 250 case SDL_QUIT: 251 application.handleQuit(); 252 break; 253 default: 254 break; 255 } 256 } 257 258 protected bool reinitPending; 259 } 260 261 /// Wraps SDL library errors. 262 class SdlException : Exception 263 { 264 this(string message) { super(message); } /// 265 } 266 267 /// ditto 268 T sdlEnforce(T)(T result, string message = null) 269 { 270 if (!result) 271 throw new SdlException("SDL error: " ~ (message ? message ~ ": " : "") ~ to!string(SDL_GetError())); 272 return result; 273 } 274 275 /// Translation table from SDL key indices to `Key`. 276 Key[SDL_NUM_SCANCODES] sdlKeys; 277 278 shared static this() 279 { 280 sdlKeys[SDL_SCANCODE_UP ] = Key.up ; 281 sdlKeys[SDL_SCANCODE_DOWN ] = Key.down ; 282 sdlKeys[SDL_SCANCODE_LEFT ] = Key.left ; 283 sdlKeys[SDL_SCANCODE_RIGHT ] = Key.right ; 284 sdlKeys[SDL_SCANCODE_PAGEUP ] = Key.pageUp ; 285 sdlKeys[SDL_SCANCODE_PAGEDOWN] = Key.pageDown; 286 sdlKeys[SDL_SCANCODE_HOME ] = Key.home ; 287 sdlKeys[SDL_SCANCODE_END ] = Key.end ; 288 sdlKeys[SDL_SCANCODE_SPACE ] = Key.space ; 289 sdlKeys[SDL_SCANCODE_ESCAPE ] = Key.esc ; 290 }